<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Rabinarayan Patra - Blog</title>
    <link>https://www.rabinarayanpatra.com</link>
    <description>Technical articles, tutorials, and insights on web development.</description>
    <language>en</language>
    <lastBuildDate>Wed, 19 Aug 2026 01:23:04 GMT</lastBuildDate>
    <atom:link href="https://www.rabinarayanpatra.com/rss.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title><![CDATA[How to Read and Write PEM Files in Java (JDK 27 PEM API)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/read-write-pem-files-java-pem-api</link>
      <guid>https://www.rabinarayanpatra.com/blogs/read-write-pem-files-java-pem-api</guid>
      <pubDate>Tue, 18 Aug 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Learn how to read PEM files in Java using the new PEM API (JEP 538). Decode and encode keys, certificates, and CRLs, plus encrypt private keys in JDK 27.]]></description>
      <content:encoded><![CDATA[<p>For twenty-five years, the answer to "how do I read a private key from a PEM file in Java" was the same embarrassing dance. Strip the <code>-----BEGIN-----</code> line. Strip the <code>-----END-----</code> line. Kill the newlines. Base64-decode what's left. Wrap it in a <code>PKCS8EncodedKeySpec</code>. Push it through a <code>KeyFactory</code>. Hope you picked the right algorithm string.</p>
<p>I have written that exact block of code in at least four projects. Every time it felt wrong, because it is wrong. PEM is a 30-year-old format with a published spec, and the standard library made me parse it by hand with <code>String.replace</code>.</p>
<p>JDK 27 finally fixes this. JEP 538 ships a real PEM API in <code>java.security</code>: <code>PEMEncoder</code> and <code>PEMDecoder</code>, which read and write keys, certificates, and CRLs without a single <code>replace</code> call. This post covers the old pain in detail, the new two-line replacement, how to encrypt private keys correctly, how to handle files with multiple PEM blocks, and exactly what changed between the preview and the final release. If you have ever managed TLS material in a Java service, like the certificate handling I touched on in my <a href="https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs">full-stack social identity guide</a>, this is the API you have wanted for a long time.</p>
<h2 id="why-has-reading-pem-files-in-java-always-been-painful">Why has reading PEM files in Java always been painful?</h2>
<p>Reading PEM files in Java was painful because the platform never had a parser for the format, so every key load became manual string surgery. PEM is just Base64-encoded DER wrapped in <code>-----BEGIN X-----</code> and <code>-----END X-----</code> markers, but the JDK gave you no direct way to go from that text to a <code>PrivateKey</code>.</p>
<p>Here is the code I have copy-pasted across projects to load an RSA private key:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// The old way: read a PEM private key by hand</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pem </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Files</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">readString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">private-key.pem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> base64 </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pem</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">replace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">-----BEGIN PRIVATE KEY-----</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ""</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">replace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">-----END PRIVATE KEY-----</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ""</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">replaceAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\\</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">s</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ""</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">byte</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> der </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Base64</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">base64</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PKCS8EncodedKeySpec</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> spec </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> PKCS8EncodedKeySpec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">der</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">KeyFactory</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> KeyFactory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getInstance</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">RSA</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PrivateKey</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> key </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">generatePrivate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>Look at everything that can go wrong there. The header text has to match exactly, including whether it says <code>PRIVATE KEY</code> or <code>RSA PRIVATE KEY</code>. You have to know the key is PKCS#8 and not PKCS#1. You have to hardcode <code>"RSA"</code> even though the file itself describes the algorithm. And if the key is encrypted, none of this works at all.</p>
<p>Certificates were slightly less awful, because <code>CertificateFactory</code> does accept PEM-wrapped X.509 input:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Certificates were tolerable, keys were not</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CertificateFactory</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cf </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CertificateFactory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getInstance</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X.509</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">X509Certificate</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cert </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">X509Certificate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">generateCertificate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ByteArrayInputStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Files</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">readAllBytes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">cert.pem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))));</span></span></code></pre></figure>
<p>But there was no single, uniform API. Certificates went through <code>CertificateFactory</code>, keys went through <code>KeyFactory</code> plus manual stripping, CRLs went through yet another path, and encrypted keys meant pulling in Bouncy Castle. That fragmentation is the actual problem JEP 538 solves.</p>
<h2 id="what-is-the-pem-api-in-jdk-27-and-what-does-it-replace">What is the PEM API in JDK 27, and what does it replace?</h2>
<p>The PEM API is a pair of classes, <code>PEMEncoder</code> and <code>PEMDecoder</code> in <code>java.security</code>, that convert between PEM text and Java security objects through one consistent interface. JEP 538 finalizes it for JDK 27 after two preview rounds, and it replaces every hand-rolled Base64-stripping routine you have ever written.</p>
<p>The design is small on purpose. Both classes are immutable and you get an instance with a static factory:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PEMDecoder</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decoder </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PEMEncoder</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> encoder </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMEncoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>Everything that can be encoded implements a marker interface called <code>BinaryEncodable</code>. The types that implement it are the ones you actually work with:</p>
<ul>
<li><code>PrivateKey</code>, <code>PublicKey</code>, and <code>KeyPair</code></li>
<li><code>X509Certificate</code> and <code>X509CRL</code></li>
<li><code>EncryptedPrivateKeyInfo</code></li>
<li><code>PKCS8EncodedKeySpec</code> and <code>X509EncodedKeySpec</code></li>
<li><code>PEM</code>, a value type that holds a raw block the decoder did not recognize</li>
</ul>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/read-write-pem-files-java-pem-api-api-map.webp" alt="Map of the PEM API showing PEMDecoder and PEMEncoder converting between PEM text and the BinaryEncodable types" width="1600" height="905"></p>
<p>That single interface is why the API stays tiny. You do not need a different entry point per object type. You decode to the type you expect, or you decode to the interface and pattern-match on what you got.</p>
<h2 id="how-do-you-read-a-pem-file-in-java-with-pemdecoder">How do you read a PEM file in Java with PEMDecoder?</h2>
<p>You read a PEM file by calling <code>decode</code> with the class you expect back. The typed overload, <code>decode(String, Class&#x3C;S>)</code>, parses the text, Base64-decodes the body, and returns a fully built object of that type.</p>
<p>The four-step ritual from earlier collapses to this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pem </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Files</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">readString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">private-key.pem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PrivateKey</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> key </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PrivateKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>Two lines, and the decoder figured out the algorithm from the encoding. No <code>KeyFactory</code>, no hardcoded <code>"RSA"</code>, no <code>PKCS8EncodedKeySpec</code>.</p>
<p>Certificates and public keys work the same way. You change the <code>Class</code> argument and that is it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PEMDecoder</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decoder </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">X509Certificate</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cert </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">certPem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> X509Certificate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PublicKey</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pub </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pubPem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PublicKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">X509CRL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> crl </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">crlPem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> X509CRL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>The difference between the old way and the new way is stark when you put them next to each other.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/read-write-pem-files-java-pem-api-old-vs-new.webp" alt="Side by side comparison of nine lines of manual PEM parsing versus two lines using PEMDecoder" width="1600" height="905"></p>
<p>There is also a <code>decode(InputStream)</code> overload, which matters because you often read this material straight off the classpath or a socket rather than from a <code>String</code>. If the type does not match what the PEM actually contains, the decoder throws rather than handing you a silently wrong object, which is exactly the behavior you want for security material.</p>
<h2 id="how-do-you-write-objects-to-pem-with-pemencoder">How do you write objects to PEM with PEMEncoder?</h2>
<p>You write a key or certificate to PEM by passing it to <code>encodeToString</code>, which returns the full PEM text including the header and footer lines. <code>PEMEncoder</code> is the mirror image of the decoder and handles every <code>BinaryEncodable</code> type.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PEMEncoder</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> encoder </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMEncoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> certPem </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> encoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">encodeToString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cert</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> keyPem </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> encoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">encodeToString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">privateKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pubPem </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> encoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">encodeToString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">publicKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>If you need raw bytes instead of a <code>String</code>, for example to write directly to a file or a network buffer, use <code>encode</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">byte</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pemBytes </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMEncoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">encode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cert</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Files</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">write</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">cert.pem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pemBytes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>The encoder picks the correct header label for you. A <code>PrivateKey</code> becomes a <code>PRIVATE KEY</code> block, an <code>X509Certificate</code> becomes a <code>CERTIFICATE</code> block, and so on. You are no longer responsible for getting the boundary text right, which was a real source of bugs when other tools refused to parse a key because the label was slightly off.</p>
<h2 id="how-do-you-encrypt-a-private-key-in-pem-format">How do you encrypt a private key in PEM format?</h2>
<p>You encrypt a private key by chaining <code>withEncryption(password)</code> before you encode. The encoder wraps the key as an encrypted PKCS#8 structure, so the PEM you get out is an <code>ENCRYPTED PRIVATE KEY</code> block that is safe to store next to your code.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">char</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> password </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">correct-horse-battery-staple</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toCharArray</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> encryptedPem </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMEncoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withEncryption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">encodeToString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">privateKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>The default password-based encryption algorithm is <code>PBEWithHmacSHA256AndAES_128</code>. If your security policy needs a different one, set it through the <code>jdk.epkcs8.defaultAlgorithm</code> security property rather than passing it per call. That keeps the choice in one place instead of scattered through your code.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/read-write-pem-files-java-pem-api-encrypt-flow.webp" alt="Flow of a private key through withEncryption to an encrypted PEM block and back through withDecryption" width="1600" height="905"></p>
<p>Reading it back is the symmetric operation. You hand the password to the decoder with <code>withDecryption</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PrivateKey</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> key </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDecryption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">encryptedPem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PrivateKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>For more control, you can work with <code>EncryptedPrivateKeyInfo</code> directly. The encoder accepts it as input, and when you decode an encrypted block without a password, you get one back so you can inspect the algorithm before deciding how to handle it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">BinaryEncodable</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> obj </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">encryptedPem</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">obj </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">instanceof</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> EncryptedPrivateKeyInfo</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    PrivateKey</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> key </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This is a real upgrade. Before JDK 27, encrypted PKCS#8 keys were the single biggest reason teams reached for Bouncy Castle. Now it is in the platform.</p>
<h2 id="how-do-you-handle-a-file-with-multiple-pem-blocks">How do you handle a file with multiple PEM blocks?</h2>
<p>You handle multi-block files by decoding to the <code>BinaryEncodable</code> interface instead of a concrete class, then pattern-matching on each result. A chain file or a bundle often holds a private key followed by a certificate followed by a CA chain, and the untyped <code>decode</code> lets you take them one at a time.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">BinaryEncodable</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> obj </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PEMDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pemBlock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">switch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">obj</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    case</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PrivateKey</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> key </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> loadKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    case</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> X509Certificate</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cert </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> addToChain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cert</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    case</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> X509CRL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> crl </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> registerRevocations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">crl</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    case</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PEM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pemBlockData </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unrecognized block: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pemBlockData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IllegalStateException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unexpected PEM content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The <code>PEM</code> case is the interesting one. When the decoder hits a block it does not have a dedicated type for, it does not throw. It gives you a <code>PEM</code> object that exposes the raw pieces: the <code>type()</code> (the label between the dashes), the Base64 <code>content()</code>, and any <code>leadingData()</code> that appeared before the block. That last part matters for real-world files, which frequently carry human-readable comments above the actual PEM.</p>
<p>So you can round-trip even content the JDK does not understand natively, which means the API does not lock you out of custom or newer PEM labels. That is a thoughtful piece of design for a security API that has to live for decades.</p>
<h2 id="what-changed-between-the-preview-and-the-final-pem-api">What changed between the preview and the final PEM API?</h2>
<p>The biggest changes are three renames that landed when JEP 538 finalized the API, so preview code from JDK 25 and 26 needs small edits. If you tried this during preview, the concepts are identical but a few names moved.</p>
<p>The renames:</p>
<ul>
<li>The encodable interface was called <code>DEREncodable</code> in the previews. It is now <code>BinaryEncodable</code>.</li>
<li>The catch-all type was a record named <code>PEMRecord</code>. It is now an ordinary class named <code>PEM</code>, which let the team add constructors that take Base64 byte arrays with proper defensive copying.</li>
<li><code>PEMDecoder.withFactory(Provider)</code> became <code>withFactoriesOf(Provider)</code>.</li>
</ul>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/read-write-pem-files-java-pem-api-version-timeline.webp" alt="Timeline from JEP 470 preview in JDK 25 to JEP 524 second preview in JDK 26 to JEP 538 final in JDK 27" width="1600" height="905"></p>
<p>The release path explains the polish. The feature previewed as JEP 470 in JDK 25, came back as a second preview (JEP 524) in JDK 26, and is final as JEP 538 in JDK 27. As of late May 2026 it sits at Proposed to Target for 27, with the finalization review closing.</p>
<p>If you are on JDK 25 or 26 and want to try it today, you compile and run with preview features enabled:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">javac</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --release</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 26</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --enable-preview</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> PemDemo.java</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --enable-preview</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> PemDemo</span></span></code></pre></figure>
<p>On JDK 27 you drop both flags, because the API is a permanent part of the platform. That is the whole point of the two-preview cadence: the API got two rounds of real feedback before it became something you cannot change.</p>
<h2 id="should-you-adopt-the-java-pem-api-right-away">Should you adopt the Java PEM API right away?</h2>
<p>Yes, the moment you are on JDK 27, because the manual approach is not just ugly, it is a security liability. Every hand-rolled parser is a place where you can mismatch an algorithm, mishandle an encrypted key, or accept malformed input you should have rejected. Moving that logic into the platform means it gets the same scrutiny as the rest of <code>java.security</code>.</p>
<p>The thing I keep coming back to is how long we tolerated the old way. PEM parsing was a rite of passage, a snippet everyone carried in their head, and that is exactly the kind of code that should never have been ours to write. If JDK 27 is in your future, delete your PEM helper class the day you upgrade. You will not miss it.</p>
<p>For the full specification and reference, see the <a href="https://openjdk.org/jeps/538">JEP 538: PEM Encodings of Cryptographic Objects</a>, the <a href="https://seanjmullan.org/blog/2025/09/23/jdk25">JDK 25 security enhancements writeup</a> by the JDK security lead, and the <a href="https://www.infoq.com/news/2026/05/java-news-roundup-may11-2026/">InfoQ Java News Roundup</a> tracking the finalization.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-26-http-3-httpclient">Java 26 HTTP/3 in the HttpClient</a>: the other half of modern Java networking, where the keys and certificates you load with the PEM API actually get used for TLS.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs">Full-Stack Social Identity with Spring Security and Next.js</a>: a real auth flow where certificate and key handling shows up in production.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java Development with Java 21+ and Spring Boot</a>: more recent language and platform features worth folding into your services.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Measure Core Web Vitals in a Single-Page App]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/measure-core-web-vitals-spa-route-changes</link>
      <guid>https://www.rabinarayanpatra.com/blogs/measure-core-web-vitals-spa-route-changes</guid>
      <pubDate>Thu, 13 Aug 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Learn how to measure Core Web Vitals in a single-page application with Chrome's Soft Navigations API, attributing LCP, INP, and CLS to each client-side route.]]></description>
      <content:encoded><![CDATA[<p>Here is a bug that does not throw an error, does not show up in your logs, and quietly makes your performance dashboard lie to you. Your single-page app loads fast on the landing route, so your Core Web Vitals look green. Then a user clicks into a deep route, waits two seconds for a chart to paint, and your monitoring never records it.</p>
<p>I hit this on a React dashboard last year. The initial load scored a clean LCP of 1.4s. Real users on the reports page were staring at spinners for 3 seconds, and our RUM tool showed nothing wrong, because in a single-page app the browser only ever fires one navigation. Every route change after that is invisible to the performance timeline.</p>
<p>Chrome's Soft Navigations API fixes this. It teaches the browser to recognize a client-side route change as a real page view, then attributes LCP, INP, and CLS to that specific route. This post walks through what a soft navigation is, how to turn the API on, how to wire it into <code>PerformanceObserver</code> and the web-vitals library, and how to read the per-route numbers. If you have ever wondered why your SPA metrics felt too good, this is the missing piece. It pairs well with the work I did on <a href="https://www.rabinarayanpatra.com/blogs/replacing-useeffect-data-fetching-server-actions">moving data fetching off useEffect</a>, since slow route data is the usual reason a soft navigation scores badly.</p>
<h2 id="why-are-core-web-vitals-invisible-in-a-single-page-application">Why are Core Web Vitals invisible in a single-page application?</h2>
<p>Core Web Vitals are invisible on SPA route changes because the browser ties every metric to a single navigation event, and a client-side route change never fires one. When your router swaps the view with <code>history.pushState</code> and a DOM update, the browser sees the same document it loaded minutes ago. LCP was already finalized on first paint. CLS keeps summing layout shifts across the whole session. INP is the only one that survives, and even it has no idea which route the slow interaction belonged to.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/measure-core-web-vitals-spa-route-changes-blind-spot.webp" alt="Timeline showing Core Web Vitals attributed only to the first load while three SPA route changes record nothing" width="1600" height="905"></p>
<p>So you end up with a metric that describes the first thing a user saw and ignores everything after. For a content site with full page loads, that is fine. For a SPA where users spend 90% of their time on routes they reached by clicking, it is close to useless.</p>
<p>The traditional workarounds were all bad. Some teams manually called <code>performance.mark()</code> on every route change and computed their own timings, which misses the actual paint and interaction signals the browser tracks internally. Others gave up and reported only the initial load, accepting the blind spot. Neither gives you the real LCP element or the real interaction latency for a route.</p>
<p>What you actually want is for the browser to reset its Core Web Vitals accounting at each route change, the same way it would on a multi-page site. That is exactly what soft navigations provide.</p>
<h2 id="what-is-a-soft-navigation-and-how-does-chrome-detect-one">What is a soft navigation, and how does Chrome detect one?</h2>
<p>A soft navigation is a client-side route change that Chrome recognizes as a new logical page view based on a heuristic. The browser cannot read your router's mind, so it watches for a specific pattern of three things happening together.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/measure-core-web-vitals-spa-route-changes-heuristic.webp" alt="The three conditions Chrome checks to flag a soft navigation: user interaction, URL change, and visible paint" width="1600" height="905"></p>
<p>All three conditions must hold:</p>
<ol>
<li><strong>A user action initiates the navigation.</strong> A click or a keypress, not a background timer or a fetch that resolves on its own.</li>
<li><strong>The URL changes visibly.</strong> Through <code>history.pushState</code>, <code>history.replaceState</code>, or a direct History API call that updates what the user sees in the address bar.</li>
<li><strong>The interaction produces a visible paint.</strong> New content has to actually render, so a no-op route that changes the URL but paints nothing does not count.</li>
</ol>
<p>That heuristic is deliberately conservative. Chrome would rather miss a borderline navigation than wrongly split one page view into two and corrupt your numbers. The <code>replaceState</code> trigger was added to the final origin trial after developer feedback, because plenty of routers use <code>replaceState</code> for things like filter changes that users perceive as navigations.</p>
<p>The important detail for instrumentation: once Chrome flags a soft navigation, it stamps a unique <code>navigationId</code> onto the performance entries that follow. That id is the thread you pull on to group LCP, CLS, and interaction entries by route.</p>
<h2 id="how-do-you-turn-on-the-soft-navigations-api-in-chrome">How do you turn on the Soft Navigations API in Chrome?</h2>
<p>You enable the Soft Navigations API one of two ways depending on whether you want local testing or real field data. For local work, flip a flag. For production measurement, join the origin trial.</p>
<p>For local testing, enable the Chrome flag:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>chrome://flags/#soft-navigation-heuristics</span></span></code></pre></figure>
<p>Or launch Chrome from the command line with the feature turned on:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># macOS</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">/Applications/Google\</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Chrome.app/Contents/MacOS/Google</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\ </span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Chrome</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --enable-features=SoftNavigationHeuristics</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Linux</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">google-chrome</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --enable-features=SoftNavigationHeuristics</span></span></code></pre></figure>
<p>For field data from real users, register your origin in the Soft Navigations origin trial. It runs from Chrome 147 through Chrome 149, with a stable launch expected later in 2026. Once you have a token, add it as a meta tag in your document head:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="html" data-theme="material-theme github-light"><code data-language="html" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">meta</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> http-equiv</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">origin-trial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">YOUR_ORIGIN_TRIAL_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> /></span></span></code></pre></figure>
<p>Or send it as an HTTP response header, which is the better choice for a SPA because the token applies before your JavaScript runs:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>Origin-Trial: YOUR_ORIGIN_TRIAL_TOKEN</span></span></code></pre></figure>
<p>Either way, the API surface is identical. The flag is for your own Chrome during development. The token is what lets the feature run for visitors who have not enabled any flags, so your real-user monitoring actually collects data.</p>
<h2 id="how-do-you-observe-soft-navigations-with-performanceobserver">How do you observe soft navigations with PerformanceObserver?</h2>
<p>You observe soft navigations by registering a <code>PerformanceObserver</code> for the <code>soft-navigation</code> entry type. Each entry that arrives represents one detected route change, carrying the new URL and a <code>navigationId</code> you use to correlate other metrics.</p>
<p>Always feature-detect first so you do not throw on browsers without the API:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="javascript" data-theme="material-theme github-light"><code data-language="javascript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> supportsSoftNavigations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    typeof</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PerformanceObserver</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">undefined</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x26;&#x26;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    PerformanceObserver</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">supportedEntryTypes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">includes</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">soft-navigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supportsSoftNavigations</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> observer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> PerformanceObserver</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    for</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> of</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEntries</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      console</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Soft navigation to</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">name</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      console</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">navigationId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">navigationId</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      console</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">started at</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">startTime</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  observer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">observe</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">soft-navigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> buffered</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The <code>buffered: true</code> option matters. It replays entries that fired before your observer registered, which is common in a SPA where your analytics code loads after the first interaction. Without it you would miss early route changes.</p>
<p>Each <code>soft-navigation</code> entry gives you a few useful fields:</p>
<ul>
<li><code>name</code> is the new URL the navigation resolved to.</li>
<li><code>navigationId</code> is the unique key for grouping all metrics from this route.</li>
<li><code>startTime</code> is when the initiating interaction happened.</li>
<li><code>largestInteractionContentfulPaint</code> points at the largest paint that resulted from the navigation, the soft-nav equivalent of LCP.</li>
</ul>
<p>That <code>navigationId</code> is the whole game. Every Core Web Vital entry that follows a soft navigation gets the same id, so you can finally answer "how slow was the reports route specifically" instead of "how slow was the session."</p>
<h2 id="how-do-you-attribute-lcp-inp-and-cls-to-each-route">How do you attribute LCP, INP, and CLS to each route?</h2>
<p>You attribute each metric to a route by reading the <code>navigationId</code> that Chrome now attaches to standard performance entries after a soft navigation. The <code>largest-contentful-paint</code>, <code>layout-shift</code>, and <code>event</code> entries all gain that field, so grouping by it gives you per-route vitals.</p>
<p>Here is a single observer that buckets metrics by navigation:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="javascript" data-theme="material-theme github-light"><code data-language="javascript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> metricsByNavigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Map</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getBucket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">navigationId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">metricsByNavigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">has</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">navigationId</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    metricsByNavigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">navigationId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      lcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      cls</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> metricsByNavigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">navigationId</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> observer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> PerformanceObserver</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  for</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> of</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEntries</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">navigationId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ===</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> undefined</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">continue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> bucket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getBucket</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entryType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ===</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">soft-navigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      bucket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entryType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ===</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">largest-contentful-paint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      bucket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">startTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entryType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ===</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">layout-shift</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">hadRecentInput</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      bucket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cls</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">observer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">observe</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">soft-navigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> buffered</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">observer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">observe</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">largest-contentful-paint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> buffered</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">observer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">observe</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">layout-shift</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> buffered</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>The difference this makes is stark once you compare it to the old single-load view.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/measure-core-web-vitals-spa-route-changes-before-after.webp" alt="Before and after table contrasting one set of session-wide vitals with per-route vitals keyed by navigationId" width="1600" height="905"></p>
<p>INP needs one extra note. Interaction entries map to a soft navigation through the <code>interactionId</code>, and the spec guidance is to use <code>interactionId</code> rather than <code>navigationId</code> when you correlate <code>interaction-contentful-paint</code> entries. For most reporting you will let the web-vitals library handle that wiring, which is the next section.</p>
<h2 id="how-do-you-report-per-route-vitals-with-the-web-vitals-library">How do you report per-route vitals with the web-vitals library?</h2>
<p>You report per-route vitals by using the experimental soft-navigation build of Google's web-vitals library, which exposes a <code>reportSoftNavs</code> option on each metric function. That option tells the library to emit a fresh metric object for every soft navigation instead of one per page load.</p>
<p>Import the soft-navs build and opt in per metric:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="javascript" data-theme="material-theme github-light"><code data-language="javascript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  onLCP</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  onINP</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  onCLS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://unpkg.com/web-vitals@soft-navs/dist/web-vitals.js?module</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> sendToAnalytics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">metric</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // metric.navigationId ties this value to a specific route</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> JSON</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> metric</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> metric</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    rating</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> metric</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">rating</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    navigationId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> metric</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">navigationId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  navigator</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sendBeacon</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/analytics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> body</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">onLCP</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(sendToAnalytics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> reportSoftNavs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">onINP</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(sendToAnalytics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> reportSoftNavs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">onCLS</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(sendToAnalytics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> reportSoftNavs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>With <code>reportSoftNavs: true</code>, the callback fires once for the initial load and again for each detected route change, and every metric object carries the <code>navigationId</code> so your backend can group by route. The standard web-vitals v5 build still works for the hard load, but only the soft-navs build resets the metrics per soft navigation.</p>
<p>A few practical notes from wiring this into a real RUM pipeline:</p>
<ul>
<li>Use <code>navigator.sendBeacon</code> or a <code>fetch</code> with <code>keepalive: true</code> so the report survives the user leaving the route.</li>
<li>Send the resolved route pattern, not the raw URL, if your routes have ids in them. Reporting <code>/orders/:id</code> instead of <code>/orders/8412</code> keeps your dashboard groupable.</li>
<li>Keep the hard-load report too. The first load is still your most important view and you want both in the same dataset.</li>
</ul>
<p>If you already report Core Web Vitals from a framework like Next.js, you are swapping the import path and adding the <code>reportSoftNavs</code> flag. The shape of your analytics payload barely changes.</p>
<h2 id="how-do-you-read-soft-navigation-data-in-chrome-devtools">How do you read soft navigation data in Chrome DevTools?</h2>
<p>You read soft navigation data directly in the Performance panel, which has shown soft-nav markers since Chrome 145. Record a trace, click through a few client-side routes, and DevTools draws a marker at each detected soft navigation so you can see exactly where the browser reset its metrics.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/measure-core-web-vitals-spa-route-changes-devtools.webp" alt="A soft navigation marker in the Chrome DevTools Performance panel timeline" width="1122" height="712">
<em>Source: <a href="https://developer.chrome.com/docs/web-platform/soft-navigations">Chrome for Developers</a></em></p>
<p>The markers are the fastest way to confirm the heuristic is firing on your routes before you invest in a full RUM integration. If you click a link and no marker appears, one of the three conditions failed. Usually it is the visible-paint requirement, which trips when a route renders from cache so fast that Chrome does not register a contentful paint, or when the URL changed without a user interaction the browser could attribute.</p>
<p>Record the trace, watch for the markers, and line them up against the LCP and layout-shift entries in the same timeline. That visual check has saved me from shipping instrumentation that silently recorded nothing on half my routes.</p>
<h2 id="what-are-the-limits-of-the-soft-navigations-api">What are the limits of the Soft Navigations API?</h2>
<p>The biggest limit is that this is a heuristic running only in Chromium during an origin trial, so it is a sample and not a complete picture. You need to design your reporting around that from day one rather than treating soft-nav data as ground truth for every user.</p>
<p>The constraints worth planning around:</p>
<ul>
<li><strong>Chromium only.</strong> Firefox and Safari have no equivalent. Your soft-nav numbers describe Chrome users. Keep your hard-load metrics as the cross-browser baseline.</li>
<li><strong>It is a heuristic.</strong> Routes that paint from cache instantly, or navigations not tied to a clear user interaction, can be missed. The detection favors precision over recall, so expect some false negatives rather than false positives.</li>
<li><strong>Origin trial status.</strong> The API ran as an origin trial through Chrome 149 with a stable launch targeted for later in 2026. Field tokens expire, and the surface could still shift slightly before it ships, though the team froze most of it for this final trial.</li>
<li><strong>CrUX is undecided.</strong> Google has been explicit that this trial is about evaluating the API, not about how the data feeds the Chrome User Experience Report. So do not assume soft-nav vitals will show up in your CrUX dashboard or affect search signals yet.</li>
</ul>
<p>None of that makes the API less worth adopting. A Chromium-only sample of your real per-route performance is infinitely more than the zero data you have today. Just label it honestly in your dashboards so nobody mistakes a Chrome sample for the whole population.</p>
<h2 id="is-the-soft-navigations-api-worth-adopting-now">Is the Soft Navigations API worth adopting now?</h2>
<p>Yes, even as an origin trial, because a Chromium-only sample of real per-route performance beats the zero data you have today. The reason this matters goes beyond a nicer dashboard. For years, SPA teams optimized the one route the browser could measure and flew blind on everything else, which quietly trained a generation of apps to be fast on the homepage and slow everywhere that mattered. Per-route Core Web Vitals change the incentive. Once you can see that the settings page has a 4-second LCP, you fix the settings page.</p>
<p>My advice: turn on the flag this week and record a Performance trace of your own app clicking through its three most-used routes. The markers will tell you in 30 seconds whether your performance story is as good as your current metrics claim. I would bet it is not, and that is exactly the point.</p>
<p>For the full specification and reference, see the <a href="https://developer.chrome.com/docs/web-platform/soft-navigations">Soft Navigations API documentation</a>, the <a href="https://developer.chrome.com/blog/final-soft-navigations-origin-trial">final origin trial announcement</a>, and the <a href="https://github.com/GoogleChrome/web-vitals">web-vitals library on GitHub</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/replacing-useeffect-data-fetching-server-actions">Why I Replaced useEffect Data Fetching with Server Actions</a>: slow route-change data is the most common reason a soft navigation scores a bad LCP, so this is the upstream fix.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a Modern Documentation Generator with Next.js 16</a>: a practical look at the kind of client-side routing that produces soft navigations in the first place.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/nextjs-may-2026-security-release-upgrade-guide">How to Upgrade Next.js for the May 2026 Security Release</a>: keeping your framework current is what gives you access to the newest performance APIs like this one.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Migrate Micronaut 4 to Micronaut 5 with JDK 25]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/migrate-micronaut-4-to-micronaut-5-jdk-25</link>
      <guid>https://www.rabinarayanpatra.com/blogs/migrate-micronaut-4-to-micronaut-5-jdk-25</guid>
      <pubDate>Tue, 11 Aug 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Migrate Micronaut 4 to Micronaut 5 step by step. JDK 25 setup, Gradle 9.5 + Maven snippets, Jackson 3, JSpecify, retry API, and real upgrade pitfalls.]]></description>
      <content:encoded><![CDATA[<p>Micronaut 5.0.0 hit GA on May 20, 2026. First major Micronaut release in roughly three years, and the migration is heavier than the version number suggests.</p>
<p>I ran the upgrade on a Micronaut 4.7 service last week. The BOM bump is one line. Everything after that is real work: Java 25, Jackson 3, JSpecify imports, a renamed security processor, RxJava 2 gone, MicroStream replaced, and a new programmatic retry API that the old annotation flow quietly redirects to. This guide walks the path I took, with the exact Gradle and Maven snippets that compiled cleanly on my service.</p>
<h2 id="what-changed-between-micronaut-4-and-micronaut-5">What changed between Micronaut 4 and Micronaut 5?</h2>
<p>Micronaut 5 is a platform-wide refresh across 70+ modules, not just a framework bump. The headline changes that affect real services are: JDK 25 baseline (was JDK 17), Kotlin 2.3, Groovy 5, a refactored IoC container with tighter qualifier semantics, JSpecify nullability replacing the older annotation soup, Jackson 3 in <code>micronaut-jackson-databind</code>, HTTP/3 promoted to stable on the Netty stack, and a new programmatic retry and circuit breaker API alongside the existing annotation model.</p>
<p>On the deprecation side, Bootstrap Configuration is marked for removal in Micronaut 6, RxJava 2 is fully dropped (you go to RxJava 3), and MicroStream support is replaced by EclipseStore. If you used <code>micronaut-eclipsestore-annotations</code>, that artifact is renamed too.</p>
<p>The piece I underestimated was the annotation processor rename. Security in particular silently broke my build until I swapped the artifact ID. More on that below.</p>
<h2 id="how-do-you-install-jdk-25-for-the-migration">How do you install JDK 25 for the migration?</h2>
<p>Use SDKMAN. It is the cleanest way to flip JDK versions per project without touching system PATH.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sdk</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 25-tem</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sdk</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> default</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 25-tem</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Verify</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -version</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># openjdk version "25" 2025-09-16</span></span></code></pre></figure>
<p>Add a <code>.sdkmanrc</code> to your project root so the JDK pins automatically when you cd into it:</p>
<pre><code>java=25-tem
</code></pre>
<p>If you run on a Docker base image, switch your runtime stage to <code>eclipse-temurin:25-jre</code> or <code>bellsoft/liberica-openjdk-alpine:25</code>. I ran a quick perf check after the JDK bump alone, before touching Micronaut: startup dropped roughly 8% on my service from JDK 21 to JDK 25, before any framework gains. Your numbers will vary, but it is a real improvement.</p>
<p>For more on what is coming after JDK 25, see my deep-dive on <a href="https://www.rabinarayanpatra.com/blogs/java-26-structured-concurrency-jep-525">Java 26 Structured Concurrency</a>.</p>
<h2 id="how-do-you-update-gradle-builds-for-micronaut-5">How do you update Gradle builds for Micronaut 5?</h2>
<p>Bump three coordinates: the Micronaut platform BOM, the Micronaut Gradle plugin, and Gradle itself. Then add the Kotlin and Shadow plugin updates if you use them.</p>
<p><strong>Before (Micronaut 4.7.x):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="gradle" data-theme="material-theme github-light"><code data-language="gradle" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>plugins {</span></span>
<span data-line=""><span>  id("io.micronaut.application") version "4.4.4"</span></span>
<span data-line=""><span>  id("com.gradleup.shadow") version "8.3.5"</span></span>
<span data-line=""><span>  id("org.jetbrains.kotlin.jvm") version "2.0.21"</span></span>
<span data-line=""><span>  id("org.jetbrains.kotlin.plugin.allopen") version "2.0.21"</span></span>
<span data-line=""><span>  id("com.google.devtools.ksp") version "2.0.21-1.0.27"</span></span>
<span data-line=""><span>}</span></span>
<span data-line=""> </span>
<span data-line=""><span>micronaut {</span></span>
<span data-line=""><span>  version("4.7.6")</span></span>
<span data-line=""><span>  runtime("netty")</span></span>
<span data-line=""><span>  testRuntime("junit5")</span></span>
<span data-line=""><span>}</span></span>
<span data-line=""> </span>
<span data-line=""><span>java {</span></span>
<span data-line=""><span>  sourceCompatibility = JavaVersion.VERSION_17</span></span>
<span data-line=""><span>  targetCompatibility = JavaVersion.VERSION_17</span></span>
<span data-line=""><span>}</span></span></code></pre></figure>
<p><strong>After (Micronaut 5.0.0):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="gradle" data-theme="material-theme github-light"><code data-language="gradle" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>plugins {</span></span>
<span data-line=""><span>  id("io.micronaut.application") version "5.0.0"</span></span>
<span data-line=""><span>  id("com.gradleup.shadow") version "9.4.1"</span></span>
<span data-line=""><span>  id("org.jetbrains.kotlin.jvm") version "2.3.21"</span></span>
<span data-line=""><span>  id("org.jetbrains.kotlin.plugin.allopen") version "2.3.21"</span></span>
<span data-line=""><span>  id("com.google.devtools.ksp") version "2.3.7"</span></span>
<span data-line=""><span>}</span></span>
<span data-line=""> </span>
<span data-line=""><span>micronaut {</span></span>
<span data-line=""><span>  version("5.0.0")</span></span>
<span data-line=""><span>  runtime("netty")</span></span>
<span data-line=""><span>  testRuntime("junit5")</span></span>
<span data-line=""><span>}</span></span>
<span data-line=""> </span>
<span data-line=""><span>java {</span></span>
<span data-line=""><span>  sourceCompatibility = JavaVersion.VERSION_25</span></span>
<span data-line=""><span>  targetCompatibility = JavaVersion.VERSION_25</span></span>
<span data-line=""><span>}</span></span></code></pre></figure>
<p>Then run <code>./gradlew wrapper --gradle-version=9.5.0 --distribution-type=bin</code> once to upgrade the wrapper. Commit the changed wrapper files in the same PR as the Micronaut bump so CI runs against the matching Gradle.</p>
<p>If you read the version catalog instead of inline versions, you only update <code>gradle/libs.versions.toml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="toml" data-theme="material-theme github-light"><code data-language="toml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">versions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">micronaut </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">5.0.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">micronaut-plugin </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">5.0.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">kotlin </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.3.21</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ksp </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.3.7</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">shadow </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">9.4.1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<h2 id="how-do-you-update-maven-builds-for-micronaut-5">How do you update Maven builds for Micronaut 5?</h2>
<p>For Maven, swap the <code>micronaut-parent</code> coordinate and set the Java release properties to 25. Nothing else in the POM should change for a clean Micronaut 4 → 5 upgrade.</p>
<p><strong>Before:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">parent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.micronaut.platform</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">micronaut-parent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">4.7.6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">parent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">jdk.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">17</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">jdk.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">release.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">17</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">release.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p><strong>After:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">parent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.micronaut.platform</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">micronaut-parent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">5.0.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">parent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">jdk.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">25</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">jdk.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">release.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">25</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">release.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Make sure your Maven version is 3.9 or later. If you use the Micronaut Maven plugin for native-image builds, it pulls the matching version from the parent BOM, so you do not need to pin it separately.</p>
<h2 id="how-do-you-adopt-jspecify-nullability-annotations">How do you adopt JSpecify nullability annotations?</h2>
<p>Micronaut 5 standardizes on JSpecify for nullability. If your codebase mixes <code>org.jetbrains.annotations.Nullable</code>, <code>javax.annotation.Nullable</code>, and <code>io.micronaut.core.annotation.Nullable</code>, this is the cleanup moment.</p>
<p>The replacement is consistent: any package-level, class-level, or method-level nullability comes from <code>org.jspecify.annotations</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Before (mixed across the codebase)</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jetbrains</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> io</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">micronaut</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">core</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">NonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NonNull</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Nullable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tenant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ...</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// After</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jspecify</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">NonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jspecify</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NonNull</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Nullable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tenant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ...</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>For broader scope, mark whole packages as non-null by default with a <code>package-info.java</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NullMarked</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">package</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">example</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jspecify</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">NullMarked</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>That single annotation says every reference in the package is non-null unless explicitly marked <code>@Nullable</code>. It cuts annotation noise and gives Kotlin call sites cleaner platform types. Add JSpecify as a direct dependency only if you compile against it outside Micronaut classes; Micronaut transitively brings it in.</p>
<p>IntelliJ has a built-in inspection called "Migrate nullability annotations". Set the target to JSpecify and run it on your <code>src/main</code> tree. Review every change, then commit. Do not let the inspection touch generated code.</p>
<h2 id="why-does-jackson-3-break-existing-serialization">Why does Jackson 3 break existing serialization?</h2>
<p><code>micronaut-jackson-databind</code> now uses Jackson 3 exclusively, and Jackson 3 has real wire-compatible differences from Jackson 2. The two ones that bit me:</p>
<p>First, the package root changed. Custom <code>JsonSerializer</code> and <code>JsonDeserializer</code> classes that import <code>com.fasterxml.jackson.databind.*</code> need to move to <code>tools.jackson.databind.*</code>. The class names are the same, the package is different.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Before (Jackson 2)</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">fasterxml</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jackson</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">databind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">JsonSerializer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">fasterxml</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jackson</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">databind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">SerializerProvider</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// After (Jackson 3)</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jackson</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">databind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">JsonSerializer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">jackson</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">databind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">SerializerProvider</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Second, Jackson 3 fails fast on unknown properties by default. If you relied on lenient deserialization in production, set the right deserialization feature on your <code>ObjectMapper</code> at startup, or annotate the DTO:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">JsonIgnoreProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">ignoreUnknown</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> OrderEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createdAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>I had three DTO classes that were silently dropping unknown fields in Micronaut 4 and started throwing <code>UnrecognizedPropertyException</code> in Micronaut 5. Run integration tests against real payloads before merging.</p>
<p>If you prefer to stay on the Micronaut serialization stack (<code>micronaut-serde-jackson</code>), this section does not apply. Serde is unchanged across the upgrade.</p>
<h2 id="what-changed-in-the-security-annotation-processor">What changed in the security annotation processor?</h2>
<p>The annotation processor artifact ID was renamed for several modules in Micronaut 5. The most important one is <code>micronaut-security-annotations</code>, which became <code>micronaut-security-processor</code>.</p>
<p>The compiler error is misleading. You get a stack of "cannot find symbol" or "annotation processor failed to load" messages that point at your own code, not at the missing processor.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">&#x3C;!-- Before (Maven, annotationProcessorPaths) --></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.micronaut.security</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">micronaut-security-annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">&#x3C;!-- After --></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.micronaut.security</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">micronaut-security-processor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="gradle" data-theme="material-theme github-light"><code data-language="gradle" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>// Before (Gradle)</span></span>
<span data-line=""><span>annotationProcessor("io.micronaut.security:micronaut-security-annotations")</span></span>
<span data-line=""> </span>
<span data-line=""><span>// After</span></span>
<span data-line=""><span>annotationProcessor("io.micronaut.security:micronaut-security-processor")</span></span></code></pre></figure>
<p>The same rename pattern applies to <code>micronaut-eclipsestore-annotations</code>, which becomes <code>micronaut-eclipsestore-processor</code>. If you use Views Turbo, the dependency moved out of the main views module: add <code>micronaut-views-turbo</code>, and rename <code>@TurboView</code> to <code>@TurboStreamView</code> at every usage.</p>
<h2 id="how-do-the-new-programmatic-retry-and-circuit-breaker-apis-work">How do the new programmatic retry and circuit breaker APIs work?</h2>
<p>Micronaut 4 only let you opt in to retry and circuit breakers through annotations like <code>@Retryable</code> and <code>@CircuitBreaker</code>. Micronaut 5 adds a programmatic, typed API in <code>io.micronaut.retry</code> that you can call without an annotation, which is what you want when the retry policy depends on runtime config.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> io</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">micronaut</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">retry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">RetryPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> io</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">micronaut</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">retry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">RetryState</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">inject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Singleton</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Singleton</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PaymentClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> RetryPolicy</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> retryPolicy </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> RetryPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">maxAttempts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">delay</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">200</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">multiplier</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">includes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">IOException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PaymentResult</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> charge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ChargeRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> retryPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">charge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The circuit breaker variant uses the same builder shape with <code>circuitBreaker(true)</code>, an open-state duration, and a half-open probe count. It lets you wire policy from <code>application.yml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ConfigurationProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">payments.retry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PaymentsRetryConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> maxAttempts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Duration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> delay</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> double</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> multiplier</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>I switched two services from annotation-only to programmatic retry during the upgrade. The win is testability: you inject a stub <code>RetryPolicy</code> in unit tests instead of waiting on real backoff timers.</p>
<p>The old annotations still work. You only need the programmatic API where the policy is dynamic. Mixing both in the same service is fine.</p>
<h2 id="what-other-breaking-changes-should-you-plan-for">What other breaking changes should you plan for?</h2>
<p>A few smaller items can stall the build if you do not catch them up front.</p>
<ul>
<li><strong>RxJava 2 is dropped.</strong> Add <code>micronaut-rxjava3</code> and migrate <code>io.reactivex.*</code> imports to <code>io.reactivex.rxjava3.*</code>. If you only used RxJava 2 for HTTP client return types, switching to <code>Mono</code> and <code>Flux</code> via Reactor is the cleaner long-term move.</li>
<li><strong>MicroStream is replaced by EclipseStore.</strong> Same APIs in most places, but the artifact and package roots moved to <code>org.eclipse.store</code>. If you use the Micronaut integration, depend on the <code>eclipsestore</code> modules.</li>
<li><strong>Testcontainers wiring changed.</strong> Use <code>org.testcontainers:testcontainers-junit-jupiter</code> for JUnit 5 integration, not the bare <code>junit-jupiter</code> artifact. The old coordinate compiles but produces a <code>NoClassDefFoundError</code> at test time.</li>
<li><strong>Data embedded fields need an annotation.</strong> Annotate embedded fields with <code>@MappedProperty</code> so the column names stay stable. If you want the old behavior, set <code>micronaut.data.embedded.naming.strategy=LEGACY</code> in <code>application.yml</code>.</li>
<li><strong>Bootstrap Configuration deprecated.</strong> Still works in Micronaut 5, scheduled for removal in Micronaut 6. If you load critical secrets through bootstrap, plan the migration to <code>PropertySourceImporter</code> SPI.</li>
</ul>
<p>After fixing these, a clean rebuild should pass. If your test suite still fails on dependency injection, regenerate the IDE project files. IntelliJ caches the old annotation-processor outputs and will mislead you for an hour before you give up and reimport.</p>
<h2 id="what-should-you-do-after-the-micronaut-5-upgrade-is-live">What should you do after the Micronaut 5 upgrade is live?</h2>
<p>Once the service compiles and tests pass, do four things before merging:</p>
<ol>
<li>Run an integration test pass against real upstream payloads, especially anything that hits Jackson. The strict unknown-property default will catch dirty data you forgot you were ignoring.</li>
<li>Re-baseline startup time and memory in production. The IoC refactor and JDK 25 combined cut my startup by ~14% on a six-controller service. You want a number you can show to your platform team.</li>
<li>Audit your <code>@Replaces</code>, <code>@Requires</code>, and qualifier annotations. The compile-time semantics tightened, and a couple of ambiguous-bean errors that Micronaut 4 silently resolved by ordering now throw at startup. The error message names both candidates, which is the fix.</li>
<li>Update your CI base images to JDK 25. I forgot this for one repo and got a green local build with a broken <code>mvn deploy</code> job. Pin the image, not just the toolchain.</li>
</ol>
<p>The platform refresh is the biggest Micronaut release since 4.0 in 2023, and it sets the floor for what a Java microservice baseline looks like for the next two years.</p>
<p>For more on the Micronaut 5 release, see the <a href="https://micronaut.io/2026/05/20/micronaut-framework-5-0-0-released/">official Micronaut 5.0.0 announcement</a>, the <a href="https://github.com/micronaut-projects/micronaut-core/wiki/Update-to-Micronaut-5">Update to Micronaut 5 guide</a>, and the <a href="https://www.infoq.com/news/2026/05/java-news-roundup-may18-2026/">Java News Roundup May 18, 2026</a> on InfoQ.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java + Spring Boot</a>. Compare the Micronaut upgrade path with how the same JDK 25 baseline reshapes a Spring Boot service.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-26-structured-concurrency-jep-525">Java 26 Structured Concurrency: What Changed in the Sixth Preview</a>. The next JDK feature your Micronaut 5 service can adopt once the upgrade is in.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">Java Libraries Beyond Lombok</a>. Useful JVM libraries that pair well with a JDK 25 + Micronaut 5 stack.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Add Audit Trails with Hibernate 7.4 @Audited Annotation]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/hibernate-7-4-audited-annotation-spring-boot</link>
      <guid>https://www.rabinarayanpatra.com/blogs/hibernate-7-4-audited-annotation-spring-boot</guid>
      <pubDate>Thu, 06 Aug 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Hibernate 7.4 ships a built-in @Audited annotation. Step-by-step how-to wires it into Spring Boot, defines a @Changelog entity, and queries via AuditLog.]]></description>
      <content:encoded><![CDATA[<p>I have wired Envers into Spring Boot projects three times in the last five years and every time it felt like work that should have been a single annotation. Hibernate 7.4 finally makes it one. The new <code>@Audited</code> annotation lives in <code>org.hibernate.annotations</code> (the core package, not the Envers module), and it is the first piece of a much larger feature called the StateManagement SPI.</p>
<p>This post walks through the full setup in a Spring Boot 4 project: how to override the Hibernate version to get 7.4, how to wire the <code>@Changelog</code> entity that supplies changeset ids, how to annotate an entity to start auditing, and how to query history with the new <code>AuditLog</code> interface. The feature is incubating, meaning the API can shift before the final release, but the surface is small enough that the migration cost is bounded if it does.</p>
<p>If you already use Envers, the migration path is gradual. You can keep auditing existing entities with Envers and adopt the new annotation only on new ones until the dust settles.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/hibernate-7-4-audited-annotation-spring-boot.webp" alt="How to Add Audit Trails with Hibernate 7.4 @Audited Annotation cover" width="1600" height="905"></p>
<h2 id="what-is-the-audited-annotation-in-hibernate-74">What is the @Audited annotation in Hibernate 7.4?</h2>
<p>The new <code>@Audited</code> annotation marks an entity class (or a collection field) as audited, meaning Hibernate keeps a historical record of every change to it. It lives in <code>org.hibernate.annotations.Audited</code> and was added in Hibernate 7.4 as an <code>@Incubating</code> feature, shipped first in <code>7.4.0.CR1</code> on May 7, 2026.</p>
<p>When you annotate an entity with <code>@Audited</code>, Hibernate creates two tables instead of one. The primary table holds the current state, exactly like before. A separate audit log table holds a row for every change. Each audit row contains:</p>
<ul>
<li>The full state of the entity at the moment of the change (minus any fields you mark with <code>@Excluded</code>).</li>
<li>A <code>changesetId</code> column that groups changes into atomic batches.</li>
<li>A <code>modificationType</code> column encoded as <code>0</code> for creation, <code>1</code> for modification, and <code>2</code> for deletion.</li>
</ul>
<p>The <code>changesetId</code> is supplied by one of three mechanisms, in order of preference: a <code>@Changelog</code> entity in your domain model (the recommended path), a custom <code>ChangesetIdentifierSupplier</code> registered via configuration, or, as a last resort, <code>Instant.now()</code>. The Hibernate javadoc itself flags relying on the <code>Instant.now()</code> fallback as not recommended.</p>
<p>You query history three ways. You open a session with <code>SessionBuilder.atChangeset(id)</code> to transparently read entity state as of that changeset (regular HQL queries just work). You use the <code>AuditLog</code> interface for programmatic access to revision history and cross-entity queries. Or you open a session with <code>AuditLog.ALL_CHANGESETS</code> and write custom HQL using the new <code>changesetId()</code> and <code>modificationType()</code> functions.</p>
<p>This is the entire model. No revision-info entity. No revision listener. No conditional auditing strategy. Just an annotation, a changelog entity, and a query interface.</p>
<h2 id="how-does-hibernate-74-audited-differ-from-envers">How does Hibernate 7.4 @Audited differ from Envers?</h2>
<p>The Envers module (introduced in Hibernate 3.6 over a decade ago) lives in <code>org.hibernate.envers</code> and ships as a separate jar. It uses a revision-tracking model with <code>RevisionEntity</code> and <code>@RevisionNumber</code>, writes audit tables prefixed with <code>_AUD</code>, and queries through <code>AuditReader</code>. It works and is mature, but it is also crusty: configuration is XML-shaped, the API surface is wide, and getting current-user-in-audit-trail right always involves a custom listener.</p>
<p>The new annotation rebuilds the same idea on a smaller surface. Key differences:</p>
<ul>
<li><strong>Package</strong>: <code>org.hibernate.annotations.Audited</code> (new core annotation) versus <code>org.hibernate.envers.Audited</code> (legacy Envers annotation). Both annotations are spelled <code>@Audited</code>, so the import you choose decides which engine handles the entity.</li>
<li><strong>Module</strong>: the new annotation is in <code>hibernate-core</code>. No extra dependency needed if you already have Hibernate 7.4 on your classpath. Envers ships in <code>hibernate-envers</code> and must be added separately.</li>
<li><strong>Changeset metadata</strong>: Envers uses a <code>RevisionEntity</code> with <code>@RevisionNumber</code> and <code>@RevisionTimestamp</code>. The new annotation uses a <code>@Changelog</code> entity with <code>@ChangesetId</code> and <code>@Timestamp</code>. The shape is similar but the field annotations are first-class on the new side.</li>
<li><strong>Audit table layout</strong>: Envers writes a <code>_AUD</code> suffixed table per entity with revision linkage. The new model writes a parallel audit table with <code>changesetId</code> plus <code>modificationType</code> columns directly. Different layout, different query SQL.</li>
<li><strong>Querying</strong>: Envers exposes <code>AuditReader</code> retrievable from the <code>EntityManager</code>. The new model exposes <code>AuditLog</code> through <code>AuditLogFactory.create()</code> and supports transparent point-in-time reads via <code>SessionBuilder.atChangeset()</code>.</li>
<li><strong>Maturity</strong>: Envers is GA, battle-tested, used in production for a decade. The new annotation is incubating, so the API can shift between 7.4 and 7.5.</li>
</ul>
<p>The Hibernate team has stated the 7.4 release is backward compatible with Envers, meaning your existing Envers-audited entities keep working unchanged when you upgrade. You can layer the new <code>@Audited</code> annotation on new entities while leaving the old ones on Envers, then migrate over time.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/hibernate-7-4-audited-annotation-spring-boot-comparison.webp" alt="Hibernate 7.4 @Audited vs Envers comparison table" width="1600" height="905"></p>
<h2 id="how-do-you-set-up-the-audit-feature-in-a-spring-boot-project">How do you set up the audit feature in a Spring Boot project?</h2>
<p>You override the Hibernate version managed by Spring Boot, because Spring Boot 4.0 ships Hibernate 7.0 and Spring Boot 4.1 has not been released yet. The override is a single property in your build file.</p>
<p>For Maven (<code>pom.xml</code>):</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">java.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">24</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">java.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">hibernate.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">7.4.0.CR1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">hibernate.version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>For Gradle (<code>build.gradle.kts</code>):</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ext[</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"hibernate.version"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">] </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> "7.4.0.CR1"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">dependencies</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    implementation</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"org.springframework.boot:spring-boot-starter-data-jpa"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Spring Boot picks up the override and Hibernate Core, Hibernate JPA, and Hibernate Annotations all align on 7.4.0.CR1.</p>
<p>If you already had Envers on the classpath, leave it. The new annotation is in core. You can keep both jars side by side and import explicitly. If you intentionally do not want Envers anymore, exclude it once you have migrated every entity.</p>
<p>Verify the version on application startup. Hibernate logs it at INFO level on the bootstrap line:</p>
<pre><code>INFO  o.h.Version - HHH000412: Hibernate ORM core version 7.4.0.CR1
</code></pre>
<p>There is no extra configuration required for the audit feature to activate. The presence of a <code>@Changelog</code> entity and any <code>@Audited</code> entity is enough.</p>
<h2 id="how-do-you-define-a-changelog-entity-for-changeset-ids">How do you define a @Changelog entity for changeset ids?</h2>
<p>Add a single class to your domain model annotated with <code>@Changelog</code> and <code>@Entity</code>. It must declare an <code>@Id</code> field with <code>@ChangesetId</code> and a timestamp field with <code>@Timestamp</code>. Hibernate handles the rest, persisting one instance per transaction and using its primary key as the changeset id for every audited change in that transaction.</p>
<p>A minimal version:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">package</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">example</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">audit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Entity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">GeneratedValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">GenerationType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Table</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Changelog</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ChangesetId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CreationTimestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Entity</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Changelog</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Table</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">changeset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Changeset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Id</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ChangesetId</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GeneratedValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">strategy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> GenerationType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">IDENTITY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Timestamp</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">CreationTimestamp</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createdAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getCreatedAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createdAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>@CreationTimestamp</code> populates <code>createdAt</code> automatically on insert, which the <code>@Changelog</code> Javadoc explicitly cites as the recommended way to initialize the timestamp field. The <code>Instant</code> type maps cleanly to a PostgreSQL <code>timestamp</code> column.</p>
<p>Once this class is on the classpath, Hibernate auto-registers it as the changeset id supplier. You do not need to set <code>org.hibernate.cfg.StateManagementSettings.CHANGESET_ID_SUPPLIER</code> manually. Only one entity per application may be annotated with <code>@Changelog</code>, so this is your single source of truth for changeset metadata.</p>
<p>To enrich the audit trail with the current user and an optional comment, add fields and a <code>@ChangesetListener</code>. The listener fires on every changeset insertion and gives you a hook to populate the user from your security context:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">package</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">example</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">audit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">audit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ChangesetListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">security</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">core</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">context</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">SecurityContextHolder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CurrentUserChangesetListener</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ChangesetListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> prePersist</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Object</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> changeset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">changeset </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">instanceof</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Changeset</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> c</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> SecurityContextHolder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAuthentication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            c</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setCreatedBy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> ?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> :</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">system</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>You register the listener once during <code>SessionFactory</code> build via the Hibernate <code>Integrator</code> SPI, the same way you would have wired any other Hibernate listener.</p>
<h2 id="how-do-you-annotate-an-entity-to-start-auditing-it">How do you annotate an entity to start auditing it?</h2>
<p>Annotate the entity class with <code>@Audited</code>. That is the entire setup. Hibernate generates the audit log table on schema export, populates it on every change, and you do not have to write a single line of audit-specific code anywhere else.</p>
<p>A complete example for a <code>Customer</code> aggregate:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">package</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">example</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Entity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">GeneratedValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">GenerationType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Table</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Audited</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Excluded</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Entity</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Audited</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Table</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Id</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GeneratedValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">strategy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> GenerationType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">IDENTITY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> unique</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Excluded</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> passwordHash</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // getters and setters</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The <code>@Excluded</code> annotation on <code>passwordHash</code> keeps that column out of the audit log, which is what you want for password hashes, PII you do not need to track historically, or large binary blobs. Everything else is recorded on every change.</p>
<p>When Hibernate exports the schema, it creates two tables: the regular <code>customer</code> table and a parallel audit log table. Hibernate names the audit table conventionally, similar to <code>customer_aud</code> by default, and the <code>@Audited</code> annotation accepts custom table name overrides if you need them. The audit table contains the same state columns minus excluded ones, plus a <code>changeset_id</code> column and a <code>modification_type</code> column.</p>
<p>A typical row sequence after a create-update-delete cycle on a single customer:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">SELECT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> FROM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customer_aud </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">WHERE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 42</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> ORDER BY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> changeset_id;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id | </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">name</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      | email          | changeset_id | modification_type</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">----+-----------+----------------+--------------+-------------------</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 42</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> | Alice Doe | alice@ex.com   |            </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> | </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 42</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> | Alice Doe | alice@xyz.com  |            </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> | </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 42</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> | Alice Doe | alice@xyz.com  |            </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">3</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> | </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2</span></span></code></pre></figure>
<p>The <code>0/1/2</code> encoding is from the source javadoc directly. You can decode it in queries using the new <code>modificationType()</code> HQL function.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/hibernate-7-4-audited-annotation-spring-boot-schema.webp" alt="Audit log table schema generated by @Audited" width="1600" height="905"></p>
<h2 id="how-do-you-query-audit-history-with-the-auditlog-interface">How do you query audit history with the AuditLog interface?</h2>
<p>Obtain an <code>AuditLog</code> instance from <code>AuditLogFactory.create()</code>, run your history queries, and close it when done. The <code>AuditLog</code> interface is <code>AutoCloseable</code> and manages its own internal session, so a try-with-resources block is the cleanest pattern.</p>
<p>A repository method that returns the full revision history of a customer:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">package</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">example</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">util</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">audit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">AuditLog</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">audit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">AuditLogFactory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">stereotype</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Repository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Repository</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CustomerAuditRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AuditLogFactory</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CustomerAuditRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">AuditLogFactory</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">factory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getHistory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> customerId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">AuditLog</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auditLog </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auditLog</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getHistory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customerId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The <code>getHistory(Class, Object)</code> method returns every revision of the entity in chronological order, including the final deletion row if the entity has been deleted. Each returned instance is a snapshot of the entity at that changeset, not a live managed object.</p>
<p>For more specific queries (e.g., who deleted what last quarter), open a session with the <code>AuditLog.ALL_CHANGESETS</code> magic value and write custom HQL using the new functions:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> sessionFactory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">atChangeset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">AuditLog</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ALL_CHANGESETS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> deletions </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createSelectionQuery</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"""</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            select c.id, c.email, changesetId(c), changeset.createdBy</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            from Customer c</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            join Changeset changeset on changesetId(c) = changeset.id</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            where modificationType(c) = 2</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">              and changeset.createdAt > :since</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            """</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[].</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setParameter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">since</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lastQuarterStart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getResultList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>changesetId(c)</code> returns the changeset of the audit row. <code>modificationType(c)</code> returns the 0/1/2 code. Joining against the <code>Changeset</code> entity lets you correlate with whatever metadata you put on your changelog (user, comment, request id).</p>
<h2 id="how-do-you-read-entity-state-at-a-past-changeset">How do you read entity state at a past changeset?</h2>
<p>Open a session with <code>SessionBuilder.atChangeset(changesetId)</code> and run regular HQL queries. Every entity load, every query, every association traversal returns the state as of that changeset, not the current state.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Customer</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> customerAtChangeset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customerId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> changesetId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> sessionFactory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">atChangeset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">changesetId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createSelectionQuery</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">from Customer where id = :id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                Customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setParameter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customerId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">uniqueResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This is the killer feature versus Envers, which required different APIs to query past state. Here the same HQL works whether the session is rooted at the current state or a historical one, because the session itself carries the temporal anchor.</p>
<p>Use cases I have already mapped to this:</p>
<ul>
<li><strong>Audit trail UI</strong>: render a customer detail page as it looked on a specific date by passing the changeset id from a date picker.</li>
<li><strong>Debug session reconstruction</strong>: when a customer reports a bug from yesterday, open a session at yesterday's last changeset and run the user's flow against the historical data.</li>
<li><strong>Compliance exports</strong>: for GDPR Article 30 records of processing, you can prove exactly what data existed when, without parsing audit log tables yourself.</li>
</ul>
<p>One important caveat from the source: <code>atChangeset()</code> sessions are read-only. Attempting a flush or commit raises an exception. This is by design (you cannot retroactively edit history), but it means you cannot use the same session for both historical reads and current writes.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/hibernate-7-4-audited-annotation-spring-boot-query-flow.webp" alt="How to query Hibernate audit log via AuditLog interface and atChangeset session" width="1600" height="905"></p>
<h2 id="what-should-you-do-this-week">What should you do this week?</h2>
<p>I would not migrate production audit code to 7.4 yet. It is <code>@Incubating</code>, the API can shift before final release, and Envers is still production-grade. But I would:</p>
<ol>
<li>Spin up a 7.4 sandbox project and try the annotation on one entity. The full setup above takes about thirty minutes.</li>
<li>File feedback on anything that feels off (the Hibernate team is active on the issue tracker and incubating-annotation feedback is the whole point of the CR phase).</li>
<li>Plan the migration path for after the 7.4 final release. The Envers-to-new-annotation walk is straightforward because both can coexist.</li>
</ol>
<p>If you start a new Spring Boot 4 project from scratch right now and want auditing, the choice is harder. Envers is mature but verbose. The new annotation is clean but unstable. I would still pick Envers today for production, the new annotation for greenfield experiments.</p>
<p>Either way, the days of writing your own audit-trail listeners and updating-by-hand audit columns should be behind you. Hibernate has caught up to what every team eventually builds by hand.</p>
<p>For more on the new feature, see the <a href="https://docs.hibernate.org/orm/7.4/whats-new/">Hibernate 7.4 What's New page</a>, the <a href="https://github.com/hibernate/hibernate-orm/blob/main/hibernate-core/src/main/java/org/hibernate/annotations/Audited.java">Hibernate Audited.java source on GitHub</a>, and the <a href="https://docs.jboss.org/hibernate/orm/7.4/userguide/html_single/Hibernate_User_Guide.html#envers">official Envers documentation</a> for the legacy module.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-4-aot-data-repositories">Spring Boot 4 AOT Data Repositories: The Underrated Feature</a>. Companion piece on Spring Boot 4 data layer improvements that pair well with the new audit setup.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/postgresql-18-temporal-foreign-keys-spring-boot">PostgreSQL 18 Temporal Foreign Keys with Spring Boot JPA</a>. Database-side temporal data. Complements the application-side audit log with row-level effective dating.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Hibernate Lazy Initialization Guide</a>. Same kind of Hibernate-runtime detail post if you want more of this depth.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Set Up Kiro Crew for Unattended Coding Tasks]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-set-up-kiro-crew-unattended-coding-tasks</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-set-up-kiro-crew-unattended-coding-tasks</guid>
      <pubDate>Tue, 04 Aug 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Kiro Crew setup from install to unattended runs: prerequisites, kiro-cli auth, the gateway, systemd service, and the sandbox settings that actually matter.]]></description>
      <content:encoded><![CDATA[<p>AWS open sourced Kiro Crew on August 4, 2026, and the pitch is one most of us have quietly wanted for a while: start a piece of work, close the laptop, and come back to something reviewable instead of a dead session and a cold context window.</p>
<p>I want to be upfront about where I sit. I work at Amazon and I have contributed to Kiro Crew. Everything below comes from the public release: the <a href="https://kiro.dev/blog/introducing-kiro-crew/">announcement post</a>, the <a href="https://kiro.dev/crew/">product page</a>, and the <a href="https://github.com/kirodotdev/KiroCrew">Apache 2.0 repository</a>. No internal detail, no roadmap talk. Just the setup path and the parts I think are worth slowing down on.</p>
<p>This is a Kiro Crew setup guide, not a review. By the end you will have a gateway running, a service that survives a reboot, and enough understanding of the sandbox to decide what you are comfortable letting it do while you sleep.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-kiro-crew-unattended-coding-tasks.svg" alt="Kiro Crew keeps a task alive across sessions: you start work and leave, the gateway checkpoints and retries, and you return to a reviewable result"></p>
<h2 id="what-is-kiro-crew-and-how-is-it-different-from-a-coding-agent">What is Kiro Crew and how is it different from a coding agent?</h2>
<p>Kiro Crew is a persistent workspace that coordinates agents across sessions, where a normal coding agent lives and dies inside one conversation. That difference sounds small and it is not. A single-session agent forgets everything when the process exits, which means anything longer than one sitting gets restarted by hand. Crew keeps memory, schedules, approvals, and in-flight task state in a local gateway, so a job started Tuesday afternoon can still be making progress Wednesday morning.</p>
<p>It started inside Amazon as a side project called MeshClaw, built by three engineers who wanted to kick off work and walk away. AWS says it reached more than 39,000 Amazon builders and roughly 500 contributors in under six months of internal use before the public release.</p>
<p>The architecture is three pieces:</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Job</th>
</tr>
</thead>
<tbody>
<tr>
<td>Gateway</td>
<td>Routes messages, persists state, manages sessions, approvals, memory, and security policy</td>
</tr>
<tr>
<td>Agent Sessions</td>
<td>Isolated conversation or task contexts</td>
</tr>
<tr>
<td>ACP Runtime</td>
<td>Drives <code>kiro-cli</code> over the Agent Client Protocol</td>
</tr>
</tbody>
</table>
<p>Two open standards do the heavy lifting. Agent Client Protocol (ACP) is how the gateway drives and observes the agent, and Model Context Protocol (MCP) is how tools get attached. If you have read my take on <a href="https://www.rabinarayanpatra.com/blogs/why-mcp-2026-07-28-spec-goes-stateless">why the MCP spec went stateless</a>, the shape here will feel familiar.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-kiro-crew-unattended-coding-tasks-architecture.svg" alt="Kiro Crew architecture: the gateway holds state and policy, agent sessions stay isolated, and the ACP runtime drives kiro-cli"></p>
<h2 id="what-do-you-need-before-running-kiro-crew-setup">What do you need before running Kiro Crew setup?</h2>
<p>You need three things: Python 3.10 or higher, an authenticated <code>kiro-cli</code>, and hardware you are willing to leave running. Node.js 20 or 22+ only matters if you build the dashboard from source, so if you install a prebuilt wheel or the desktop app you can skip it entirely.</p>
<p>The one people miss is <code>kiro-cli</code>. It is the agent backend, not an optional extra, and Crew cannot do anything without it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kiro-cli</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> login</span></span></code></pre></figure>
<p>Do this before anything else. Skipping it produces an <code>AcpTimeoutError: ACP prompt timed out</code> later, which reads like a network problem and is not.</p>
<p>Memory search needs no setup. On first start, the in-process <code>llama-cpp-python</code> runtime pulls the Qwen3-Embedding-0.6B model, about 610 MB. Budget the disk and the first-run wait. Search comes back empty until that download lands, which is expected rather than broken.</p>
<p>One billing note that matters more for unattended work than interactive work. The software is free under Apache 2.0 and runs on your own machine, but agent inference still needs a Kiro account, and model requests from scheduled or background tasks count against your Kiro plan exactly like IDE usage. Steps that only run scripts, with no model call, cost nothing. A cron job that wakes an agent every fifteen minutes is a billing decision, not just a config line.</p>
<h2 id="how-do-you-install-kiro-crew">How do you install Kiro Crew?</h2>
<p>Pick the install method that matches how long you intend to keep it running. There are five, and they are not interchangeable.</p>
<p><strong>One-line install.</strong> Fastest path, and the right default:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -fsSL</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://download.crew.kiro.dev/cli.sh</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> sh</span></span></code></pre></figure>
<p>That script pins to the stable channel. You can target others:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -fsSL</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://download.crew.kiro.dev/cli.sh</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> sh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -s</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --channel</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> insider</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -fsSL</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://download.crew.kiro.dev/cli.sh</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> sh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -s</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --version</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0.1.0</span></span></code></pre></figure>
<p>Worth saying plainly: piping a remote script into a shell executes whatever that URL serves. It is the documented install path and the domain is AWS-operated, but if you are on a machine where that tradeoff is not acceptable, use the wheel or source path instead.</p>
<p><strong>Docker</strong>, for a server you do not want to babysit:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">docker</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> run</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -d</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --name</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kirocrew</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -p</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 127.0.0.1:5476:5476</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -v</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kirocrew-home:/home/kirocrew</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  ghcr.io/kirodotdev/kirocrew:stable</span></span></code></pre></figure>
<p>Note the <code>127.0.0.1:</code> prefix on the port binding. Dropping it exposes the dashboard on every interface, which you do not want.</p>
<p><strong>From source</strong>, if you plan to contribute or read the code:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> clone</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://github.com/kirodotdev/KiroCrew.git</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">cd</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> KiroCrew</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">make</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> build</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">source</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .venv/bin/activate</span></span></code></pre></figure>
<p><strong>Self-contained wheel</strong>, when you want a clean install without the Node toolchain on the target box:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">make</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> wheel</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">pip</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> dist/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">*</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">.whl</span></span></code></pre></figure>
<p><strong>Desktop bundle</strong>, which embeds a Python interpreter so end users need neither Python nor npm:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">make</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> desktop</span></span></code></pre></figure>
<p>That drops a signed <code>.dmg</code> on macOS or an <code>.AppImage</code> on Linux into <code>website/electron/dist/</code>.</p>
<h2 id="how-do-you-verify-the-setup-and-start-the-gateway">How do you verify the setup and start the gateway?</h2>
<p>Run the wizard, then <code>doctor</code>, then the gateway, in that order:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kirocrew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> setup</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kirocrew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> doctor</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kirocrew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gateway</span></span></code></pre></figure>
<p><code>kirocrew setup</code> is interactive and covers agent config, workspace directory, optional Slack credentials, slash-command name, timezone, dashboard URL, the Playwright MCP server, and the macOS desktop app. Answering "n" to Slack turns off that integration and leaves the dashboard fully working, so there is no reason to wire up chat on the first pass.</p>
<p>Three flags narrow what it touches:</p>
<ul>
<li><code>--agent-only</code> installs agent config and skips every credential prompt</li>
<li><code>--clean</code> rebuilds agent config from defaults and ignores what is already there</li>
<li><code>--electron-only</code> installs just the macOS desktop app</li>
</ul>
<p>The <code>--agent-only --clean</code> combination is the fix for a stale MCP config. If the agent starts timing out after you have changed tools around, rebuild before you start debugging anything else.</p>
<p><code>kirocrew doctor</code> is the command to reach for whenever something is off. It reports platform details, data home, agent config, config values, MCP servers, runtime environment, vector memory status, speech-to-text, Slack, and connectivity. Three failures cover most of what you will hit:</p>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AcpTimeoutError: ACP prompt timed out</code></td>
<td><code>kiro-cli</code> missing, not logged in, or broken MCP config</td>
<td>Install it, run <code>kiro-cli login</code>, then <code>kirocrew setup --agent-only --clean</code></td>
</tr>
<tr>
<td>Memory and knowledge search return nothing</td>
<td>Embedding model still downloading</td>
<td>Check the Vector Memory section in <code>doctor</code>, search upgrades itself once the model lands</td>
</tr>
<tr>
<td>Gateway will not start</td>
<td>Port already taken</td>
<td><code>kirocrew gateway --port auto</code></td>
</tr>
</tbody>
</table>
<p><code>kirocrew gateway</code> starts the server on <code>http://localhost:5476</code>. It binds to loopback unless you deliberately configure otherwise, which is the correct default and worth leaving alone.</p>
<p>Configuration lives at <code>~/.kiro/crew/config.json</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">agent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">provider</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">acp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">approval_mode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">interactive</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">sandbox</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">auto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">timeout_secs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1800</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">pool_size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 2</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">dashboard</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">bot_name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Kiro Crew</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Edit it through <code>kirocrew config get</code>, <code>set</code>, and <code>edit</code> rather than by hand, so validation runs. A few environment variables override the basics: <code>KIROCREW_HOME</code> for the data directory, <code>KIROCREW_PORT</code> for the dashboard port, and <code>KIROCREW_EMBED_MODEL_URL</code> if you need to point the embedding download at a mirror.</p>
<h2 id="how-do-you-keep-kiro-crew-running-when-you-close-your-laptop">How do you keep Kiro Crew running when you close your laptop?</h2>
<p>Install it as a system service, because a foreground <code>kirocrew gateway</code> dies with your terminal and takes the point of the tool with it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kirocrew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> service</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kirocrew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> service</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> status</span></span></code></pre></figure>
<p>On Linux this writes <code>/etc/systemd/system/kirocrew.service</code> and prompts for sudo. The gateway itself still runs as your user, never as root, which is the behavior you want. On macOS it writes a launchd plist instead.</p>
<p>Linux users on Ubuntu 23.10 or newer should expect one specific failure. Crew isolates the agent from your credentials using user and mount namespaces, and recent Ubuntu restricts unprivileged user namespaces through AppArmor. The error looks like this:</p>
<pre><code>sandbox: unshare(NEWNS) failed: errno 1
</code></pre>
<p><code>kirocrew service install</code> adds the required <code>kirocrew-userns</code> AppArmor profile automatically. If you are running outside systemd, invoke it yourself:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">aa-exec</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -p</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kirocrew-userns</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kirocrew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gateway</span></span></code></pre></figure>
<p>The installer skips the profile on distributions where it does not apply, so Debian, Arch, RHEL, and Amazon Linux users will not see this at all.</p>
<p>One thing that surprised me the first time: uninstalling does not remove <code>~/.kiro/crew</code>. Config, credentials, memory, and sessions all survive, and no uninstall path deletes them. That is a sensible default, but it means a "clean reinstall" is not clean unless you move the directory yourself. Back it up before you touch it.</p>
<h2 id="what-does-the-security-model-actually-block">What does the security model actually block?</h2>
<p>Kiro Crew ships seven layers of defense, and the ones that matter for unattended work are the sandbox, the deny patterns, and the audit trail. The full set is OS sandboxing, denied command patterns, bash blocking, input and output validation, sensitive path protection, credential redaction, and signed audit logs.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-kiro-crew-unattended-coding-tasks-security.svg" alt="The Kiro Crew defense layers, from OS sandbox through deny patterns and credential redaction to the signed audit log"></p>
<p>Concretely, here is what you get:</p>
<ul>
<li>The dashboard binds to <code>127.0.0.1</code> unless you configure otherwise</li>
<li>137 deny patterns block destructive and exfiltration-shaped commands</li>
<li>Sensitive path guards keep the agent out of protected directories</li>
<li>Credential redaction strips secret-shaped environment variables and patterns from output</li>
<li>Security events append to <code>~/.kiro/crew/security_events.jsonl</code></li>
<li>Linux uses namespaces, macOS uses Seatbelt, with standard, strict, and off modes</li>
</ul>
<p>Optional policy files compose tightest-wins, and enterprise settings enforce caps that runtime config cannot loosen. That ordering is the right way round, and it is the detail I would check first if you are deploying this anywhere shared.</p>
<p>Read <code>security_events.jsonl</code> after your first unattended run. Not because you expect trouble, but because it is the fastest way to learn what your agent actually reached for when nobody was watching. The gap between what I assumed a task would touch and what it tried to touch was the most useful thing I learned in my first week.</p>
<p>The Windows situation deserves a direct warning rather than a footnote.</p>
<p><strong>On Windows, Kiro Crew has no OS-level sandboxing.</strong> Because of that it refuses to spawn subprocesses by default, and the only way to change that is opting in through <code>sandbox_allow_unsandboxed_exec</code>. Turning that on means an unattended agent runs commands on your machine with no OS isolation between it and everything else. There is also no Windows desktop app yet, so you install from source. If you want unattended runs, put this on Linux or macOS. Use Windows for interactive work where you are approving each step.</p>
<h2 id="what-breaks-when-you-walk-away">What breaks when you walk away?</h2>
<p>The honest answer is that autonomy shifts your work from writing code to reviewing it, and the failure modes move with it. Crew handles the mechanical parts well: checkpoints and retries carry long tasks through transient failures, heartbeat monitoring watches PRs and deploys until state changes, and corrections you make become durable lessons stored as editable Markdown.</p>
<p>What it cannot do is notice that the task was wrong. A well-specified job runs beautifully unattended. A vague one produces a large, confident, wrong diff that costs more to review than it would have cost to write. Scope discipline matters more here than in interactive work, because there is no midpoint where you glance at the screen and course-correct.</p>
<p>Two settings shape this directly. <code>approval_mode</code> set to <code>interactive</code> means you gate tool calls, which defeats overnight runs but is the right place to start. <code>session.timeout_secs</code>, 1800 by default, bounds how long a session can sit before it gives up. Loosen both deliberately, one at a time, once you trust a particular workflow.</p>
<p>If you want a gentler on-ramp, the launch Apps are scoped jobs rather than open-ended autonomy. Issue Radar triages issues and PRs, Task Runner handles bounded long-running work, DevFleets manages worktrees, and Code Review Sage weights findings by blast radius. Starting there teaches you the review rhythm without betting a repository on it. The same instinct applies to <a href="https://www.rabinarayanpatra.com/blogs/claude-code-routines-ci-automation">scheduling agents in CI</a>: narrow scope first, widen only after you trust the output.</p>
<h2 id="should-you-set-up-kiro-crew-today">Should you set up Kiro Crew today?</h2>
<p>If you already work with agents and keep hitting the session boundary, yes. That is the specific problem Crew solves, and it solves it in a way you can audit, since the whole thing is Apache 2.0 and runs on your hardware.</p>
<p>If you are new to agentic tooling, install it but keep <code>approval_mode</code> on <code>interactive</code> for a couple of weeks. Watch what it reaches for. The persistence is the feature, and persistence without understanding is how you end up reviewing a hundred commits you did not ask for.</p>
<p>The thing I did not expect is how much the review rhythm changes. Interactive agent work is a conversation. Unattended agent work is closer to managing a very fast, very literal contractor who never asks clarifying questions. That is a different skill, and it is worth building deliberately before you scale it up.</p>
<p>For more, see the <a href="https://kiro.dev/blog/introducing-kiro-crew/">Kiro Crew announcement</a>, the <a href="https://github.com/kirodotdev/KiroCrew">Apache 2.0 source on GitHub</a>, the <a href="https://github.com/kirodotdev/KiroCrew/blob/main/docs/guides/install.md">install guide</a>, and the <a href="https://agentclientprotocol.com/">Agent Client Protocol</a> that Crew uses to drive agents.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/google-antigravity">Google Antigravity: Why I Think It Changes Everything</a>. A different take on the agentic IDE idea, useful for comparing design choices.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-code-routines-ci-automation">Claude Code Routines: Async CI Automation Just Became Real</a>. The same scheduled-agent problem approached from the CI side.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent">How to Build a Self-Hosted Slack AI Bot with any CLI Agent</a>. Relevant if you plan to drive Crew from Slack.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Recover echarts-for-react After the May 2026 AntV Attack]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/recover-echarts-for-react-may-2026-antv-attack</link>
      <guid>https://www.rabinarayanpatra.com/blogs/recover-echarts-for-react-may-2026-antv-attack</guid>
      <pubDate>Tue, 04 Aug 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Step-by-step incident response after the May 19 @antv npm worm hit echarts-for-react. Audit lockfile, pin safe versions, rotate tokens, scan history.]]></description>
      <content:encoded><![CDATA[<p>I got the Slack notification on the morning of May 20 from a dashboard project I do not even maintain anymore. The CI had failed overnight with a weird npm install error. By the time I logged in, the actual story had broken on Hacker News. A compromised npm maintainer named <code>atool</code> had published 639 malicious versions across 323 packages in a 22-minute burst on May 19, 2026. The hit list included <code>echarts-for-react</code>, with 1.1 million weekly downloads, and every major package under <code>@antv</code>.</p>
<p>If your team ran <code>npm install</code> between roughly 01:39 and 02:06 UTC on May 19, your CI runner or developer machine probably executed the payload. The malware was a <code>preinstall</code> script that harvested over 20 credential types and shipped them to attacker-controlled GitHub repos before the install even finished resolving.</p>
<p>This post is not a postmortem. It is a recovery playbook. If you shipped anything during the bad window, work through these steps in order. The first three are about containment. The last three are about hardening so the next wave does not land.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/recover-echarts-for-react-may-2026-antv-attack.webp" alt="How to Recover echarts-for-react After the May 2026 AntV Attack cover" width="1600" height="905"></p>
<h2 id="what-happened-in-the-may-2026-antv-npm-attack">What happened in the May 2026 @antv npm attack?</h2>
<p>On May 19, 2026, an attacker (tracked as TeamPCP by Socket, attributed by Microsoft as part of the Mini Shai-Hulud campaign) compromised the npm maintainer account <code>atool</code>. Over a 22-minute window, the attacker published 639 malicious versions spanning 323 packages, including the full <code>@antv</code> data-visualization suite, <code>echarts-for-react</code>, <code>timeago.js</code>, <code>size-sensor</code>, and <code>canvas-nest.js</code>.</p>
<p>The payload was a <code>preinstall</code> script with the entry <code>"bun run index.js"</code>. On install, it ran a credential harvester that scraped at least 20 credential types: AWS access keys, GCP service accounts, Azure secrets, GitHub Personal Access Tokens, npm tokens, SSH private keys, kubeconfig files, and Vault tokens. It then created public GitHub repositories on stolen accounts (with names like <code>sayyadina-stillsuit-852</code>) and committed the harvested credentials there as a fallback exfiltration channel in case the primary C2 at <code>t[.]m-kosche[.]com</code> was blocked.</p>
<p>GitHub eventually invalidated 61,274 npm tokens with write access and 2FA bypass. The malicious package versions were unpublished. But anything those tokens already touched is suspect, and anything those exfiltrated credentials can still log into is fair game for an attacker. Cleanup is on you.</p>
<p>The earlier <a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack attack</a> on May 11 used a different primitive (a GitHub Actions cache-poisoning bug that produced valid SLSA Build Level 3 attestations). This attack reverted to the classic playbook: own a maintainer account, push malicious versions, wait for <code>npm install</code>. Different mechanism, same outcome.</p>
<h2 id="how-do-you-know-if-your-app-pulled-a-poisoned-echarts-for-react-version">How do you know if your app pulled a poisoned echarts-for-react version?</h2>
<p>You know if your <code>package-lock.json</code> or <code>pnpm-lock.yaml</code> or <code>yarn.lock</code> resolved <code>echarts-for-react</code> or any <code>@antv/*</code> package to a version published between May 19, 2026 01:39 UTC and 02:06 UTC. There is no ambiguity. Any install in that window pulled a poisoned tarball.</p>
<p>Run this from your project root:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># npm</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">jq</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">.packages | to_entries[] | select(.key | test("echarts-for-react|@antv/|size-sensor|timeago\\.js|canvas-nest\\.js")) | {key, version: .value.version, resolved: .value.resolved}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> package-lock.json</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># pnpm</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">echarts-for-react|@antv/|size-sensor|timeago\.js|canvas-nest\.js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pnpm-lock.yaml</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> head</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -50</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># yarn (v1)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">^(echarts-for-react|@antv/|size-sensor|timeago\.js|canvas-nest\.js)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> yarn.lock</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -A</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 2</span></span></code></pre></figure>
<p>Then check each resolved version against the npm registry publish timestamp:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> view</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> echarts-for-react@</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">version-from-lockfil</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> time</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --json</span></span></code></pre></figure>
<p>If the timestamp falls in the bad window, you are hit. If not, you are clean for that package. Repeat for every match in the lockfile.</p>
<p>If the lockfile lookup is too slow because you have dozens of <code>@antv/*</code> deps, run a faster fleet check. Grep your CI runs for <code>preinstall</code> activity on May 19:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Search CI logs for the preinstall payload signature</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bun run index.js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .github/</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ci-logs/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Or query GitHub Actions runs (gh CLI)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">gh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> run</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> list</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --workflow=ci.yml</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --created</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2026-05-19</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --json</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> conclusion,databaseId,headBranch</span></span></code></pre></figure>
<p>Any green run from that day that ran <code>npm ci</code> against a lockfile resolving to bad versions exfiltrated credentials. Treat the runner as compromised.</p>
<h2 id="which-echarts-for-react-and-antv-versions-are-safe-to-pin">Which echarts-for-react and @antv versions are safe to pin?</h2>
<p>Any version published before May 19, 2026 01:39 UTC is safe. The compromised window is bounded. Versions published after the unpublish event on May 20 are also safe, because the maintainer account has been recovered and the malicious versions are gone from the registry.</p>
<p>The fastest way to find the right pin is to list all versions with timestamps:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> view</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> echarts-for-react</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> versions</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> jq</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">.[]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> tail</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -20</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> view</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> echarts-for-react</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> time</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> jq</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">to_entries | sort_by(.value) | .[-20:]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span></code></pre></figure>
<p>Pin to the last entry whose <code>time</code> value is before <code>2026-05-19T01:39:00Z</code>. Same for every <code>@antv/*</code> package in your lockfile.</p>
<p>To enforce the pin across the workspace, use overrides. For npm:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">overrides</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">echarts-for-react</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">3.0.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">@antv/g2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">5.2.20</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">@antv/x6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.18.1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">@antv/graphin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">3.0.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>For pnpm:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># pnpm-workspace.yaml or package.json</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">pnpm</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  overrides</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    echarts-for-react</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 3.0.4</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">    "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@antv/g2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 5.2.20</span></span></code></pre></figure>
<p>The version numbers above are placeholders. Look up the actual safe versions yourself with <code>npm view</code>, because new safe versions ship continuously and a static post will go stale. Overrides force the resolver to ignore the transitive dependency graph and pin everywhere, including inside nested deps.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/recover-echarts-for-react-may-2026-antv-attack-audit-flow.webp" alt="Lockfile audit decision flow for the AntV compromise" width="1600" height="905"></p>
<h2 id="how-do-you-clean-the-lockfile-and-reinstall-safely">How do you clean the lockfile and reinstall safely?</h2>
<p>Delete <code>node_modules</code> and the lockfile, then reinstall with <code>--ignore-scripts</code> so any residual malicious <code>preinstall</code> hook in your tree does not run. This is the single most important step in the whole playbook. Skipping <code>--ignore-scripts</code> on a poisoned lockfile is what trips most teams trying to clean up.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Stop the dev server, kill any watchers</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">rm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -rf</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> node_modules</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">rm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> package-lock.json</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># (or pnpm-lock.yaml / yarn.lock)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --ignore-scripts</span></span></code></pre></figure>
<p>If you also have leftover untracked artefacts in your working tree from the poisoned install (random dot-files, generated test fixtures), use <a href="https://www.rabinarayanpatra.com/snippets/git/git-clean-untracked-safely"><code>git clean</code> with the <code>-n</code> dry run first</a> so you do not nuke a stash or unstaged work.</p>
<p>Now audit the regenerated lockfile against the same grep from earlier. The versions should all be either pre-attack or post-recovery. If anything still resolves to a bad version, your overrides did not bite and you need to track down which transitive dep is pulling it in:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> echarts-for-react</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @antv/g2</span></span></code></pre></figure>
<p><code>npm ls</code> shows the dependency chain. Whatever package brought in the bad version needs to be updated or temporarily replaced.</p>
<p>Once the lockfile is clean, run the install one more time without <code>--ignore-scripts</code> so any legitimate <code>postinstall</code> hooks (like patches or native compiles) run as expected:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> run</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> build</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> test</span></span></code></pre></figure>
<p>If the test suite passes and the build is green, your application code is back. Now you can deal with the credentials.</p>
<h2 id="how-do-you-rotate-github-actions-secrets-and-npm-tokens">How do you rotate GitHub Actions secrets and npm tokens?</h2>
<p>Rotate everything the payload could have seen. The payload ran inside <code>npm install</code>, which means it had access to whatever the runner had access to. On a typical GitHub Actions runner, that includes the entire <code>GITHUB_TOKEN</code>, every repository secret, every organization secret pulled into the job, the npm token in <code>.npmrc</code> if present, and any cloud credentials injected as env vars.</p>
<p>Walk this list in order:</p>
<ol>
<li><strong>npm tokens.</strong> Visit <code>npmjs.com</code> settings, <strong>Access tokens</strong>. Revoke every automation token used in the affected workflows. Re-create new ones, or better, migrate to <a href="https://www.rabinarayanpatra.com/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc">npm trusted publishing with OIDC</a> so you do not need long-lived tokens at all.</li>
<li><strong>GitHub repository secrets.</strong> In each affected repo, <strong>Settings</strong> then <strong>Secrets and variables</strong> then <strong>Actions</strong>. Rotate every secret used in workflows that ran on May 19 or that share runners with affected workflows. Treat shared organization secrets the same way.</li>
<li><strong>GitHub Personal Access Tokens.</strong> Go to your account <strong>Settings</strong> then <strong>Developer settings</strong> then <strong>Personal access tokens</strong>. Revoke every classic PAT and fine-grained PAT older than May 19. Re-issue with minimum required scopes.</li>
<li><strong>Cloud credentials.</strong> AWS access keys, GCP service-account keys, Azure client secrets used by the affected workflows must all be rotated. Use the cloud console to identify keys last used by the affected runner.</li>
<li><strong>SSH keys deployed to runners.</strong> If your runners had SSH keys to deploy to staging or production, rotate them. The payload reads <code>~/.ssh/</code> directly.</li>
<li><strong>Vault tokens, Kubernetes configs, Datadog API keys, anything else you mounted into the job env.</strong> All of these were enumerated and exfiltrated. Rotate every one.</li>
</ol>
<p>After rotation, audit the actual usage. AWS CloudTrail, GCP Audit Logs, and GitHub's audit log can show whether any of the rotated keys made an unusual API call between May 19 and the moment you revoked them. Anything unusual gets escalated to a full incident.</p>
<h2 id="how-do-you-scan-git-history-for-leaked-credentials">How do you scan git history for leaked credentials?</h2>
<p>Run a fresh credential scan against the affected repository because the attacker may have pushed commits with exfiltrated data back into your repo using your own write tokens, or the rotation step might surface previously committed secrets that you never noticed. Both paths happen in real incidents.</p>
<p>Two tools that work well for this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># trufflehog: live scan against the full git history</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">docker</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> run</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --rm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -v</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">$PWD</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">:/repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> trufflesecurity/trufflehog</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> file:///repo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --only-verified</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># gitleaks: faster, config-driven, can run in CI</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">docker</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> run</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --rm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -v</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">$PWD</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">:/repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> zricethezav/gitleaks</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  detect</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --source</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /repo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --verbose</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --no-banner</span></span></code></pre></figure>
<p><code>--only-verified</code> on TruffleHog means it actually tests each finding against the relevant API to confirm it is a live credential, not just a string that looks like one. That cuts noise hard.</p>
<p>For each verified finding:</p>
<ol>
<li>Revoke the credential at the source (provider console).</li>
<li>Remove the secret from history with <code>git filter-repo</code> (do this in a separate branch first, then force-push only after team sign-off, and never on a shared branch without explicit coordination). If a teammate accidentally deletes the branch holding their cleanup work, the <a href="https://www.rabinarayanpatra.com/snippets/git/git-reflog-recover-branch"><code>git reflog</code> recovery pattern</a> gets it back.</li>
<li>Add the leaked pattern to your <code>gitleaks.toml</code> baseline so it does not flag on every CI run.</li>
</ol>
<p>If the leaked credential was a database password, rotate that too. The payload could have read environment variables on developer laptops that pulled the <code>.env</code> file from disk.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/recover-echarts-for-react-may-2026-antv-attack-rotation-checklist.webp" alt="Token rotation checklist for the AntV recovery" width="1600" height="905"></p>
<h2 id="how-do-you-harden-ci-so-the-next-wave-does-not-land">How do you harden CI so the next wave does not land?</h2>
<p>Three controls would have stopped the AntV payload cold, and adopting them now means the next wave does not become your next weekend of work.</p>
<p><strong>Run <code>npm install</code> with <code>--ignore-scripts</code> everywhere in CI.</strong> Lifecycle scripts (<code>preinstall</code>, <code>install</code>, <code>postinstall</code>) are the universal attack vector for npm worms. The Microsoft postmortem explicitly recommends this. Add <code>ignore-scripts=true</code> to your <code>.npmrc</code> for CI runners and let only known-trusted packages run their scripts via an allowlist. Tools like <code>@lavamoat/allow-scripts</code> make this practical.</p>
<p><strong>Pin lockfile versions and verify with a fast registry alternative.</strong> The faster your detection, the less time you spend in the bad window. Configure Renovate or Dependabot to alert on any dependency update, and use Socket's GitHub App or Snyk to flag malicious-package indicators before merge. The AntV payload was flagged by Socket within 6.7 minutes of publication. That is faster than most teams check Slack.</p>
<p><strong>Switch publishers off long-lived npm tokens.</strong> This is what closes the underlying class. The <code>atool</code> account fell because the credential was steal-able and reusable. Migrate publishing workflows to OIDC-based trusted publishing so even a fully compromised maintainer account cannot republish from an attacker's machine. The full setup is in my <a href="https://www.rabinarayanpatra.com/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc">npm trusted publishing guide</a>.</p>
<p>For an extra layer, gate every publish workflow on a GitHub Environment with required reviewers. The OIDC token is only minted after a human approves the deployment. Combined with tag-only triggers (no publish on every main push), this means the attacker needs the source repo AND a human reviewer AND a tag push, not just a credential.</p>
<h2 id="what-should-you-do-this-week">What should you do this week?</h2>
<p>If you shipped anything that touches the npm ecosystem, do these in this order today:</p>
<ol>
<li>Run the lockfile audit grep. Identify whether you are in the bad window.</li>
<li>If yes, treat affected runner credentials as burned and rotate them.</li>
<li>Regenerate the lockfile with <code>--ignore-scripts</code>, pin safe versions via overrides.</li>
<li>Run trufflehog or gitleaks against the full git history.</li>
<li>Set <code>ignore-scripts=true</code> in <code>.npmrc</code> for CI.</li>
<li>Schedule a follow-up to migrate publishing to OIDC trusted publishing.</li>
</ol>
<p>The Mini Shai-Hulud campaign is not over. The TanStack attack on May 11 and the AntV attack on May 19 were eight days apart, and the same actor or copycats will hit another popular maintainer account before the year ends. The defenses above stop the class of attack, not just this incident.</p>
<p>For more on this, see <a href="https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/">Microsoft's Mini Shai-Hulud writeup</a>, <a href="https://socket.dev/blog/antv-packages-compromised">Socket's detection postmortem</a>, and <a href="https://www.stepsecurity.io/blog/shai-hulud-here-we-go-again-mass-npm-supply-chain-attack-hits-the-antv-ecosystem">StepSecurity's incident analysis</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc">How to Enable npm Trusted Publishing with GitHub Actions OIDC</a>. Closes the long-lived-token attack class that made this incident possible.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack npm Supply Chain Attack 2026: Detailed Breakdown</a>. The May 11 sibling attack that used valid SLSA provenance to ship malware.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/axios-npm-supply-chain-attack-2026">Axios npm Supply Chain Attack 2026: Recovery Playbook</a>. Earlier incident in the same playbook. Useful prior art on token-theft recovery.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Enable npm Trusted Publishing with GitHub Actions OIDC]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc</guid>
      <pubDate>Thu, 30 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Stop shipping long-lived npm tokens. Step-by-step guide to enable npm trusted publishing with GitHub Actions OIDC, end to end, fixing a real attack class.]]></description>
      <content:encoded><![CDATA[<p>On May 19, 2026, a compromised npm maintainer account named <code>atool</code> published 637 malicious versions across 317 packages in a single 22-minute burst. That number is not a typo. The hit list included <code>size-sensor</code> (4.2 million weekly downloads), <code>echarts-for-react</code> (3.8 million), and <code>@antv/scale</code> (2.2 million). GitHub had to invalidate 61,274 npm tokens with write permissions and 2FA bypass to stop the bleeding.</p>
<p>I read the Microsoft Security postmortem the morning after and the root cause stuck with me. Every one of those packages had a long-lived <code>NPM_TOKEN</code> somewhere. Once the attacker had the token, they could republish from a laptop in a coffee shop and the registry would accept it.</p>
<p>There is a fix for this and it has been generally available since 2025. It is called npm trusted publishing, and it replaces stealable tokens with short-lived OIDC tokens minted by your CI provider per workflow run. This post walks through the full setup with GitHub Actions, end to end, including the cleanup step most teams skip.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc.webp" alt="How to Enable npm Trusted Publishing with GitHub Actions OIDC cover" width="1600" height="905"></p>
<h2 id="what-is-npm-trusted-publishing-and-why-does-it-matter-in-2026">What is npm trusted publishing and why does it matter in 2026?</h2>
<p>npm trusted publishing is a credential-free publishing flow where your package authenticates to the npm registry using an OIDC token issued by a pre-approved CI provider, not a token you store in <code>secrets.NPM_TOKEN</code>. GitHub Actions, GitLab CI, CircleCI, and Buildkite are the supported providers today.</p>
<p>The flow is roughly this. When your workflow runs, it asks GitHub's OIDC provider for a signed JWT. That JWT contains claims like <code>repository</code>, <code>workflow</code>, and <code>ref</code>. The npm CLI sends the JWT to the npm registry. The registry checks the claims against the trusted publisher you configured on <code>npmjs.com</code> for that package. If they match, the publish succeeds.</p>
<p>There is no shared secret. No <code>NPM_TOKEN</code>. No long-lived bearer token sitting in your GitHub secrets waiting to be exfiltrated by a malicious <code>prepare</code> script. The OIDC token is minted on demand, scoped to one workflow run, and expires in minutes.</p>
<p>In 2026 this stopped being optional. The Mini Shai-Hulud worm hit the <a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack ecosystem on May 11</a> and the @antv ecosystem on May 19, both via stolen credentials. The earlier <a href="https://www.rabinarayanpatra.com/blogs/axios-npm-supply-chain-attack-2026">axios npm compromise</a> used the same playbook. Every one of these incidents had the same root primitive: a stealable, long-lived publish token.</p>
<p>Trusted publishing also gives you build provenance for free. Public packages published this way get a SLSA Build Level 3 attestation showing exactly which repository, commit, and workflow produced the artifact. Consumers can verify it with <code>npm view &#x3C;pkg> --json</code> and inspect <code>dist.attestations</code>.</p>
<h2 id="which-versions-of-npm-and-nodejs-support-trusted-publishing">Which versions of npm and Node.js support trusted publishing?</h2>
<p>You need npm CLI 11.5.1 or later and Node.js 22.14.0 or higher. Both are documented as hard floors on the official npm docs page.</p>
<p>The simplest way to check what you have is to run this on a runner image you use:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">node</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --version</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --version</span></span></code></pre></figure>
<p>If you run an older version, the publish step fails with an authentication error. There is no graceful fallback to OIDC. Older npm does not understand the new flow at all.</p>
<p>A few practical notes from setting this up across half a dozen packages:</p>
<ul>
<li>The Ubuntu 24.04 runner image already ships Node 20 and npm 10 by default. You must use <code>actions/setup-node@v6</code> (not v4 or v5) to pin a version that supports trusted publishing.</li>
<li>Self-hosted runners are not currently supported. Only cloud-hosted GitHub-hosted runners can issue OIDC tokens that the npm registry accepts.</li>
<li>Each package can only have one trusted publisher configured at a time. If you maintain a monorepo that ships multiple packages, you configure trusted publishing on each package's settings page separately.</li>
</ul>
<p>The actions/setup-node v6 action handles the npm upgrade automatically when you pin <code>node-version: '24'</code>. That single line pulls in npm 11.x.</p>
<h2 id="how-do-you-configure-the-trusted-publisher-on-npmjscom">How do you configure the trusted publisher on npmjs.com?</h2>
<p>Configuration happens once per package, in the npmjs.com web UI, before you ever touch your workflow file. The order matters. If you flip the workflow first and have not configured the publisher, the publish call fails and you scratch your head for an hour.</p>
<p>Walk through this in a browser:</p>
<ol>
<li>Log in to <code>npmjs.com</code>. Open the package settings page at <code>https://www.npmjs.com/package/&#x3C;your-package>/access</code>.</li>
<li>Scroll to the <strong>Trusted Publisher</strong> section.</li>
<li>Click <strong>GitHub Actions</strong>.</li>
<li>Fill in five fields, in order:
<ul>
<li><strong>Organization or user</strong>: your GitHub org or username (the part before the slash in <code>org/repo</code>).</li>
<li><strong>Repository</strong>: just the repo name, without the org prefix.</li>
<li><strong>Workflow filename</strong>: the filename only, with the <code>.yml</code> or <code>.yaml</code> extension. For example, <code>publish.yml</code>. Do not include the path. Do not include the <code>.github/workflows/</code> prefix.</li>
<li><strong>Environment name</strong> (optional but recommended): the name of a GitHub Environment if you want to require deployment protection rules. I use <code>npm-publish</code> for this and gate it on a required reviewer.</li>
<li><strong>Allowed actions</strong>: tick <code>npm publish</code>, <code>npm stage publish</code>, or both. For most packages, just <code>npm publish</code>.</li>
</ul>
</li>
<li>Click <strong>Save</strong>.</li>
</ol>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc-ui.webp" alt="npm Trusted Publisher settings panel on npmjs.com" width="1600" height="905"></p>
<p>The trust is pinned to the exact <code>org/repo/workflow-filename</code> triple. If you rename the workflow file later, publishing breaks. If you fork the repo, the fork cannot publish. If someone tries to publish from a different repository, the registry rejects the OIDC token.</p>
<p>This is the entire point. A stolen token from any machine cannot satisfy these claims, because the claims come from GitHub's OIDC provider and are signed.</p>
<h2 id="what-does-the-github-actions-workflow-look-like">What does the GitHub Actions workflow look like?</h2>
<p>Here is a complete <code>.github/workflows/publish.yml</code> that publishes on a <code>v*</code> tag push. Drop this into your repo, adjust the test step, and commit.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Publish to npm</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5">on</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  push</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    tags</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">v*</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">permissions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  id-token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> write</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  contents</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> read</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">jobs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  publish</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    runs-on</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ubuntu-latest</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    environment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npm-publish</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    steps</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> uses</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> actions/checkout@v5</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> uses</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> actions/setup-node@v6</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        with</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          node-version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">24</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          registry-url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://registry.npmjs.org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          package-manager-cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npm ci</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npm test</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npm publish --access public</span></span></code></pre></figure>
<p>Three lines do the actual work and they all need to be exactly right.</p>
<p><strong><code>permissions: id-token: write</code></strong> is the line that unlocks OIDC. Without it, GitHub Actions will not issue an OIDC token to the runner, and <code>npm publish</code> falls back to looking for <code>NPM_TOKEN</code>, which is not there, and fails. This permission must be at the job level or workflow level. Job level is safer because it scopes the OIDC capability to only the publish job.</p>
<p><strong><code>registry-url: 'https://registry.npmjs.org'</code></strong> tells setup-node where to point the <code>.npmrc</code> it creates on the runner. This is what wires up <code>npm publish</code> to talk to the public registry. If you publish to a private registry instead, change this URL.</p>
<p><strong><code>package-manager-cache: false</code></strong> disables the setup-node built-in npm cache. This is a recommendation from the npm docs specifically for release builds. The cache can mask reproducibility issues by pulling stale tarballs, and on a release run you want a clean install every time.</p>
<p>A few intentional choices in this file worth calling out:</p>
<ul>
<li>I use <code>environment: npm-publish</code> to gate the job on a required reviewer. The OIDC token is only minted after the reviewer approves the deployment. This is what stops a force-pushed tag from publishing.</li>
<li>The trigger is a tag push, not a branch push. Tags are immutable. Branches are not. If you publish on every <code>main</code> push, an attacker who lands a malicious commit on main can ship to npm before anyone notices. With a tag trigger, you need to also push a tag, which requires a separate manual action.</li>
<li>There is no <code>NODE_AUTH_TOKEN</code>, no <code>secrets.NPM_TOKEN</code>, no <code>.npmrc</code> write step. setup-node v6 handles the OIDC handshake transparently when <code>id-token: write</code> is set.</li>
</ul>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc-oidc-flow.webp" alt="OIDC token flow from GitHub Actions to npm registry" width="1600" height="905"></p>
<p>Push a tag and watch the workflow run. The publish step should succeed and the npm registry page for your package should show a green provenance badge.</p>
<h2 id="how-do-you-remove-long-lived-tokens-after-trusted-publishing-works">How do you remove long-lived tokens after trusted publishing works?</h2>
<p>Trusted publishing only stops attacks if you delete the old tokens. This is the step most teams skip. They configure trusted publishing, see the green badge, ship a release, and leave the old <code>NPM_TOKEN</code> sitting in GitHub secrets and the automation token enabled on npmjs.com. Now they have two ways in and one of them is still stealable.</p>
<p>Do this in the same browser session you used to configure trusted publishing:</p>
<ol>
<li>On the npm package page, go to <strong>Settings</strong> then <strong>Publishing access</strong>.</li>
<li>Select <strong>Require two-factor authentication and disallow tokens</strong>.</li>
<li>Save.</li>
</ol>
<p>This setting means the registry will refuse any publish attempt that does not come from a trusted publisher or a maintainer with 2FA-verified web access. Even a leaked automation token cannot publish anymore.</p>
<p>Then revoke the actual token:</p>
<ol>
<li>Click your avatar then <strong>Access tokens</strong>.</li>
<li>Find any automation token that was used for publishing this package.</li>
<li>Click <strong>Revoke</strong> on each.</li>
</ol>
<p>Finally, clean up GitHub secrets:</p>
<ol>
<li>In the repo, go to <strong>Settings</strong> then <strong>Secrets and variables</strong> then <strong>Actions</strong>.</li>
<li>Delete <code>NPM_TOKEN</code> from the repository secrets and any environment secrets where it lived.</li>
</ol>
<p>Run one more publish via the workflow to confirm nothing was relying on the deleted secret. If your workflow file still references <code>NODE_AUTH_TOKEN</code> or <code>secrets.NPM_TOKEN</code> anywhere, remove those references. The new flow does not need them.</p>
<h2 id="what-are-the-limitations-of-npm-trusted-publishing-today">What are the limitations of npm trusted publishing today?</h2>
<p>Trusted publishing has real edges and you need to know them before you commit. The docs are honest about what is and is not supported.</p>
<ul>
<li><strong>Self-hosted runners are not supported.</strong> Only cloud-hosted GitHub-hosted runners can issue OIDC tokens the npm registry accepts. If you run a release pipeline on your own infrastructure, trusted publishing is not yet an option for that workflow.</li>
<li><strong>One trusted publisher per package.</strong> You cannot have both GitHub Actions and GitLab CI as trusted publishers for the same package. Pick one. Multi-CI publishing remains a token-based flow.</li>
<li><strong>Provenance generation requires a public repo and a public package.</strong> If you publish a private package, the publish itself works fine over OIDC, but the SLSA Build Level 3 attestation is not generated. This is a GitHub-side restriction on which workflows can sign provenance.</li>
<li><strong>CircleCI does not get auto-provenance.</strong> It is a supported trusted publisher but provenance generation is not currently supported there.</li>
<li><strong>Other npm commands still need tokens.</strong> <code>npm install</code>, <code>npm view</code>, and <code>npm access</code> calls against a private registry still need a token. Trusted publishing only covers the publish path.</li>
</ul>
<p>The biggest practical edge is the self-hosted runner constraint. I have one project on a self-hosted runner for compliance reasons and we run the publish job in a separate workflow on a cloud-hosted runner specifically to get trusted publishing. The build still happens on the self-hosted runner. Only the final publish step runs on a GitHub-hosted runner.</p>
<h2 id="how-does-trusted-publishing-change-the-attack-surface">How does trusted publishing change the attack surface?</h2>
<p>Trusted publishing closes one attack class cleanly and leaves another wide open. Know which is which before you ship.</p>
<p>What it stops: any attacker who steals an <code>NPM_TOKEN</code> from a developer machine, a leaked <code>.npmrc</code>, a logging system, a stack trace, or a compromised CI environment cannot publish anymore. The token they stole is not what the registry checks. The registry checks an OIDC claim that only a real GitHub Actions runner inside the configured repo can produce.</p>
<p>What it does not stop: an attacker who lands a commit on your repository. The malicious commit can ride through your trusted publishing workflow and ship to npm with a valid SLSA Build Level 3 attestation. This is exactly what the TanStack worm did on May 11, 2026. The attestation was real. The commit it attested to was malicious. Provenance proves where a package was built, not whether the source was trustworthy.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc-attack-surface.webp" alt="Attack surface comparison: long-lived tokens versus OIDC short-lived tokens" width="1600" height="905"></p>
<p>To close the second class, pair trusted publishing with these:</p>
<ul>
<li><strong>Branch protection</strong> on <code>main</code> (and any tag pattern you publish from) requiring pull request reviews from a different account than the author.</li>
<li><strong>Required reviewers</strong> on the GitHub Environment that gates the publish job. This is why I use <code>environment: npm-publish</code> in the workflow above.</li>
<li><strong>Tag-trigger only.</strong> Do not publish on every main push. Require a separate tag push that a human has to perform.</li>
<li><strong>Dependency review on PRs.</strong> Catch malicious <code>package.json</code> <code>prepare</code> hooks or new transitive deps before they hit the publish job. GitHub's dependency-review-action is one option.</li>
</ul>
<p>Trusted publishing is the floor, not the ceiling. It removes the easy attack. The hard attack still works and you defend against it with code review, environment gates, and tag-based release discipline.</p>
<h2 id="what-should-you-do-this-week">What should you do this week?</h2>
<p>I do not think trusted publishing is the last credential change npm will ship. Provenance is still optional, the self-hosted runner gap is real, and the cross-CI story is messy. But the credential-free publish flow is the most useful security upgrade npm has shipped in a decade, and the cost to adopt is one configuration screen plus a small workflow change.</p>
<p>If you publish anything to npm, set this up this week. Then delete the old token. Then do not publish a package without it ever again.</p>
<p>For more on this, see the <a href="https://docs.npmjs.com/trusted-publishers/">official npm trusted publishing docs</a>, the <a href="https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/">Microsoft Security postmortem on the AntV compromise</a>, and the <a href="https://www.stepsecurity.io/blog/shai-hulud-here-we-go-again-mass-npm-supply-chain-attack-hits-the-antv-ecosystem">StepSecurity analysis of the AntV wave</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack npm Supply Chain Attack: How the May 11 Compromise Worked</a>. The first npm worm to ship with valid SLSA provenance. Explains why OIDC tokens alone are not enough.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/axios-npm-supply-chain-attack-2026">Axios npm Supply Chain Attack 2026: What Happened and How to Defend</a>. Earlier incident in the same playbook. Useful prior art on token-theft attacks.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-Driven Anomaly Detection for Supply Chain Security</a>. Detection complement to the prevention story in this post.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Enable Kubernetes 1.36 User Namespaces for Isolation]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/enable-kubernetes-1-36-user-namespaces-isolation</link>
      <guid>https://www.rabinarayanpatra.com/blogs/enable-kubernetes-1-36-user-namespaces-isolation</guid>
      <pubDate>Tue, 28 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Step-by-step Kubernetes 1.36 user namespaces tutorial. Kernel and containerd prereqs, hostUsers field, UID mapping verification, and migration.]]></description>
      <content:encoded><![CDATA[<p>User namespaces in Kubernetes hit GA on April 22, 2026, with the v1.36 Haru release. The feature has been around as alpha since v1.25 and beta since v1.30, but until now the cost of opting in was high enough that nobody outside of GKE Sandbox and a few security-forward teams used it. As of v1.36 the gate is enabled by default, the kernel and runtime requirements are met by every up-to-date distribution, and the pod-spec change is a single field.</p>
<p>I turned this on across my own staging cluster the week the release dropped. This post walks the enablement the way I ran it: the prerequisite check that decides whether your nodes can opt in at all, the smallest possible pod manifest that exercises the feature, the verification step that proves the UID mapping is doing what you expect, and the migration plan for existing workloads that break under the change.</p>
<h2 id="what-changed-with-user-namespaces-in-kubernetes-136">What changed with user namespaces in Kubernetes 1.36?</h2>
<p>User Namespaces graduated to General Availability in Kubernetes 1.36 (the Haru release, April 22, 2026), with the matching feature gate enabled by default. The pod-spec field is <code>hostUsers: false</code>. When set, the kubelet asks the container runtime to create the pod in a new user namespace, and the runtime maps the container's UID 0 to a non-privileged UID on the host.</p>
<p>The KEP behind this is <a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/127-user-namespaces/README.md">KEP-127</a>. The headline behaviors that flipped from beta to stable in v1.36 are:</p>
<table>
<thead>
<tr>
<th>Behavior</th>
<th>Before v1.36</th>
<th>v1.36 (GA)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Feature gate</td>
<td><code>UserNamespacesSupport</code> (beta)</td>
<td>Enabled by default, no gate</td>
</tr>
<tr>
<td><code>hostUsers</code> field</td>
<td>Beta in pod spec</td>
<td>Stable in pod spec</td>
</tr>
<tr>
<td>Capabilities (e.g. <code>CAP_NET_ADMIN</code>)</td>
<td>Host-scoped, full power</td>
<td>Namespaced, container-local only</td>
</tr>
<tr>
<td>Volume UID/GID translation</td>
<td>Manual chown often required</td>
<td>Kernel handles transparently via idmap mounts</td>
</tr>
<tr>
<td>Sandbox feature requirement</td>
<td>Each guarded by sub-gates</td>
<td>Folded under the main gate</td>
</tr>
<tr>
<td>Pod-level resource resize</td>
<td>Separate alpha gate</td>
<td>Pod-level in-place resize lands stable in same release</td>
</tr>
</tbody>
</table>
<p>The two changes that matter most in practice are the namespaced capabilities and the transparent UID translation on mounted volumes. A pod with <code>CAP_NET_ADMIN</code> can still tweak its own network namespace, but cannot touch the host's. A pod that mounts a host volume sees files as owned by UID 0 inside the container while the host disk still records the actual UID. No <code>chown</code> step, no init container shuffling permissions, no setgid sticky-bit cleanup.</p>
<h2 id="why-do-user-namespaces-matter-for-container-escape-attacks">Why do user namespaces matter for container escape attacks?</h2>
<p>User namespaces matter because they sever the link between "root in the container" and "root on the host". A process that achieves a container escape on a pod with <code>hostUsers: false</code> lands as an unprivileged UID on the node, which means the standard escape primitives (writing to <code>/proc/sys</code>, mounting host paths, opening privileged sockets, loading kernel modules) all fail at the kernel boundary.</p>
<p>The history here is long and bloody. Every six months a new CVE in <code>runc</code>, <code>containerd</code>, or a kernel subsystem hands an attacker root on the host because they were already root in the container and the host trusted that UID. The 2019 <code>runc</code> symlink escape (CVE-2019-5736) was patched in a week. The 2022 <code>cgroups v1</code> race (CVE-2022-0492) took longer. Then there was the 2024 <code>runc</code> LeakyVessels chain. The pattern is the same every time: container UID 0 reaches a kernel surface, and the kernel applies host UID 0 semantics.</p>
<p>User namespaces close that pattern at the source. The container still has UID 0 inside its own namespace. The kernel still applies UID 0 semantics. But the inside-the-namespace UID 0 is mapped to a non-privileged outside-the-namespace UID, so the kernel's UID-0-only operations fail when the escape lands on the host. CAP_SYS_ADMIN on the inside means namespace-local admin. CAP_SYS_ADMIN on the host? The escaped process does not have it.</p>
<p>The blast-radius difference shows up clearly when you run the same exploit twice. I did this on a non-production cluster with a deliberately vulnerable image based on the Stripe Open Source Defense write-up. Without user namespaces the escape gave shell access to the node. With <code>hostUsers: false</code> the escape produced a shell as an unmapped UID with no write access to <code>/etc</code>, no ability to mount, and no ability to load kernel modules.</p>
<p>This is defense in depth, not a silver bullet. The pod can still be malicious, the container can still be DoS'd, and a kernel bug that bypasses namespaces entirely (which is rare but does happen) gets through anyway. Pod Security Admission still applies. runAsNonRoot still applies. Seccomp still applies. User namespaces are one more layer.</p>
<h2 id="how-do-you-check-kernel-and-runtime-prerequisites">How do you check kernel and runtime prerequisites?</h2>
<p>You check the prerequisites by reading three numbers from each node: the kernel version, the container runtime version, and the storage driver. The minimum supported combination is Linux 6.3, containerd 2.0 (or CRI-O 1.30), and a filesystem that supports idmap mounts. Older kernels can work back to 5.19 with a containerd 2.0 plus overlay-with-idmap config, but 6.3 is what the official <code>kubernetes.io</code> docs settle on for the GA path.</p>
<p>Run this on each node as a quick sanity check.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Kernel version</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">uname</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -r</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: 6.3 or newer for the supported GA path</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Container runtime</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">crictl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> version</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: containerd 2.0.x or newer, or CRI-O 1.30.x or newer</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Storage driver</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">crictl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> jq</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">.config.containerd.runtimes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Or for CRI-O:</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">crio</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> storage_driver</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Confirm the kernel exposes the namespace feature</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /proc/self/ns/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> user</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: user</span></span></code></pre></figure>
<p>Two compatibility traps to watch for:</p>
<ol>
<li><strong>containerd 1.7 + Linux 5.15.</strong> The setting is honored, but you pay a measurable storage and latency penalty because overlayfs does not support idmap mounts on that kernel. Upgrade the kernel before you turn this on broadly.</li>
<li><strong>containerd 1.6 or older.</strong> The <code>hostUsers: false</code> field is silently ignored. No error, no warning, no namespace. This is the worst failure mode because pods appear to be in a user namespace until you actually verify the UID mapping.</li>
</ol>
<p>For managed Kubernetes the answer is usually simple: GKE, EKS, and AKS all ship node pools that meet the requirement on their current LTS images as of April 2026. Use the latest node image on each, and the feature is available without extra work.</p>
<p>For self-managed clusters on Ubuntu 22.04, the default kernel is 5.15, which means an HWE (hardware enablement) kernel upgrade is the smallest path. Ubuntu 24.04 ships 6.8 by default and is the easier choice. RHEL 9.4 ships 5.14, which means you need the 9.5 update for a 6.x kernel.</p>
<p>If even one node fails the prereqs, do not enable user namespaces on the whole cluster. Cordon and drain the non-compliant nodes first, or label them and use a node selector on the user-namespaced pods. Mixed clusters work, mixed-on-the-same-node configs do not.</p>
<h2 id="how-do-you-enable-user-namespaces-on-a-pod">How do you enable user namespaces on a pod?</h2>
<p>You enable user namespaces by adding a single field to the pod spec: <code>hostUsers: false</code>. Everything else is automatic. The kubelet asks the container runtime to create the pod in a new user namespace, the runtime mints a UID range, and the kernel performs transparent UID translation on every namespaced operation including volume mounts.</p>
<p>The smallest demonstrable pod is this one.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># userns-demo.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> v1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Pod</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-demo</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  hostUsers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  containers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> shell</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> debian:bookworm-slim</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sleep</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">infinity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      securityContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        runAsUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        runAsGroup</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span></span></code></pre></figure>
<p>Apply it.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> apply</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-demo.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> wait</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --for=condition=Ready</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pod/userns-demo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --timeout=60s</span></span></code></pre></figure>
<p>Now check what the container thinks its UID is, and what the node thinks the same process's UID is.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># What the container sees</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> exec</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-demo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> id</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: uid=0(root) gid=0(root) groups=0(root)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># What the node sees. Run on the node that hosts the pod.</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">PID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">$(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> get</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pod</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-demo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -o</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> jsonpath=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{.status.containerStatuses[0].containerID}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> sed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">s|.*://||</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> xargs</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -I</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">{} crictl inspect {} \</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> jq</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">.info.pid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># On the node:</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">cat</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /proc/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">$PID</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">^Uid:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: a UID far from 0, e.g. Uid: 100000 100000 100000 100000</span></span></code></pre></figure>
<p>That second number is the proof that the namespace did its job. Container thinks it is UID 0. Host kernel thinks it is UID 100000 (or whatever range the runtime picked). Every kernel operation that requires real-UID-0 privileges fails on the host side.</p>
<p>For workloads that need more than the default 65536-UID range (large multi-tenant setups), the kubelet honors the <code>--userns-uid-mapping</code> config flag. I have never needed this. The default range is enough for almost every workload.</p>
<p>To opt out per pod, just omit the field. The default is <code>hostUsers: true</code>, which is the v1.35 behavior. To opt in for an entire namespace, use <a href="https://kyverno.io">Kyverno</a> or any other admission controller to inject the field. The smallest Kyverno policy is six lines and avoids the per-deployment churn.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># kyverno-userns.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kyverno.io/v1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ClusterPolicy</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> enforce-user-namespaces</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  rules</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> set-hostusers-false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      match</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        any</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> resources</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">              kinds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Pod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">              namespaces</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">team-a</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">team-b</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      mutate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        patchStrategicMerge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">            hostUsers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span></code></pre></figure>
<h2 id="how-do-you-verify-the-uid-mapping-works-correctly">How do you verify the UID mapping works correctly?</h2>
<p>You verify the UID mapping by writing a file from inside the container and reading the on-disk ownership from the node. If the in-container ownership shows UID 0 but the on-disk ownership shows a non-zero UID in the runtime's mapping range, the namespace is doing what it claims.</p>
<p>Set this up with an <code>emptyDir</code> volume.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># userns-verify.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> v1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Pod</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-verify</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  hostUsers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  containers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> writer</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> debian:bookworm-slim</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sleep</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">infinity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      volumeMounts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> scratch</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          mountPath</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /scratch</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  volumes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> scratch</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      emptyDir</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span></code></pre></figure>
<p>Write a file from inside the container.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> exec</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-verify</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> sh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -c</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">echo hi > /scratch/test &#x26;&#x26; ls -la /scratch/test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: -rw-r--r-- 1 root root 3 ... /scratch/test</span></span></code></pre></figure>
<p>Now find the file on the node and check the on-disk ownership.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Get the host path of the emptyDir</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">PODUID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">$(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> get</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pod</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-verify</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -o</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> jsonpath=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{.metadata.uid}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">NODE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">$(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> get</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pod</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-verify</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -o</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> jsonpath=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{.spec.nodeName}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># On the node:</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> find</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /var/lib/kubelet/pods/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">$PODUID </span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">-name</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> test</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -exec</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -la</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> {}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \;</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: -rw-r--r-- 1 100000 100000 ... /var/lib/kubelet/pods/&#x3C;uid>/volumes/.../test</span></span></code></pre></figure>
<p>That UID 100000 is the host-side view of the container's UID 0. If you see UID 0 on the on-disk side instead, the namespace did not take. Common causes: containerd is 1.6.x, the kernel does not support idmap mounts, or the filesystem of the kubelet directory does not support idmap. All three appear in the kubelet logs when they fail.</p>
<p>For a stronger test, try a privileged operation that should fail.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> exec</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> userns-verify</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> sh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -c</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mount -t tmpfs none /mnt 2>&#x26;1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Expected: mount: /mnt: permission denied</span></span></code></pre></figure>
<p>Without user namespaces, root in a container can mount tmpfs inside the container's mount namespace. With user namespaces, the mount syscall lands as a non-privileged UID on the host kernel and fails. That permission-denied is the proof that namespaced capabilities are working.</p>
<h2 id="how-do-you-migrate-existing-workloads-to-user-namespaces">How do you migrate existing workloads to user namespaces?</h2>
<p>You migrate existing workloads by classifying them against three failure modes (low ports, shared-UID volumes, host paths), enabling <code>hostUsers: false</code> on the safe ones first, and rewriting the broken ones one at a time. Do not flip the whole namespace at once.</p>
<p>The three failure modes show up like this.</p>
<table>
<thead>
<tr>
<th>Failure mode</th>
<th>Symptom</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bind to port &#x3C; 1024</td>
<td><code>bind: permission denied</code> on container start</td>
<td>Add <code>capabilities: { add: [NET_BIND_SERVICE] }</code> to securityContext, or move to a port ≥ 1024 and front with a Service</td>
</tr>
<tr>
<td>Shared-UID host volume</td>
<td>Files appear owned by an unexpected UID inside the container, or chown errors</td>
<td>Use a <code>Pod</code> spec that owns the volume (recreate as <code>emptyDir</code>) or drop the host volume</td>
</tr>
<tr>
<td><code>hostPath</code> volume</td>
<td>Mount succeeds but file ops fail with EACCES</td>
<td>Re-platform off <code>hostPath</code> to a CSI driver or <code>local</code> PV, or grant explicit access via supplementalGroups</td>
</tr>
</tbody>
</table>
<p>For the low-port case, the fix is a one-line addition.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  hostUsers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  containers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> web</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-nginx:latest</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      ports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> containerPort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 80</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      securityContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        capabilities</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          add</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">NET_BIND_SERVICE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span></code></pre></figure>
<p>The capability is namespaced under user namespaces, so it grants the bind privilege inside the container without granting any host-side capability.</p>
<p>For the shared-volume case, the right answer almost always is "do not share a host volume". If you absolutely need to share with another pod, share via a NetworkVolume (PVC) or a sidecar pattern. The on-disk UID mismatch that user namespaces introduce is the kernel doing its job; do not chown your way around it.</p>
<p>The migration sequence that worked on my own staging cluster was:</p>
<ol>
<li>Pick a low-stakes deployment with no host volumes and no low ports.</li>
<li>Patch the deployment to add <code>hostUsers: false</code>.</li>
<li>Wait one full traffic cycle. Watch pod restart count and the application's own error logs.</li>
<li>If clean, move the next deployment.</li>
<li>After half a dozen deployments without issue, layer a Kyverno policy that defaults the namespace to <code>hostUsers: false</code> and start auditing the holdouts.</li>
</ol>
<p>That is the path the <a href="https://kubernetes.io/docs/tasks/configure-pod-container/user-namespaces/">Kubernetes docs recommend</a> and it is the path I would follow in production. The dangerous version is the all-at-once flip on a busy namespace, which surfaces the failure modes at the worst time.</p>
<h2 id="what-common-problems-should-you-watch-for">What common problems should you watch for?</h2>
<p>The common problems are containerd-1.6 silently ignoring the field, kernel versions that look new enough but lack idmap mounts on the kubelet's filesystem, and workloads with hidden CAP_SYS_ADMIN needs that fail in surprising ways.</p>
<p>For each, here is the signal and the fix.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Signal 1: containerd ignores hostUsers: false silently</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> exec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x3C;</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">po</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">d</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> cat</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /proc/self/uid_map</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># A namespaced pod prints something like:</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">#          0     100000      65536</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># A non-namespaced pod prints:</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">#          0          0 4294967295</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># If you set hostUsers: false and see the second line, the runtime ignored it.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Fix: upgrade containerd to >= 2.0, or move to a node where it is.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Signal 2: kubelet logs the idmap failure</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">journalctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kubelet</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -n</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 200</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -i</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> idmap</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Common error: "failed to set up user namespace mount: idmap not supported"</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Fix: upgrade to a kernel >= 6.3 (5.19 is the absolute minimum), or move the</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># kubelet root directory to a filesystem that supports idmap mounts.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Signal 3: workload fails with CAP_SYS_ADMIN-like errors</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> logs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x3C;</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">po</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">d</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">operation not permitted|permission denied</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># This often means the workload needs a capability that is no longer host-scoped.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Fix: add the specific capability to securityContext.capabilities.add. If the</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># workload needs real host-side CAP_SYS_ADMIN, it is not a candidate for user</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># namespaces. Use a privileged sidecar pattern instead.</span></span></code></pre></figure>
<p>The other quiet problem is metrics. Pods that report container PIDs to a host-side telemetry collector (for example a host-mode Prometheus exporter for cgroup stats) need to translate the PID across the namespace. The standard exporters handle this fine. Home-grown collectors usually do not. Test your observability stack on at least one user-namespaced pod before you flip a whole namespace.</p>
<p>For deeper context on the security primitives that compose with user namespaces, see the <a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">zero trust microservices guide</a> for the auth-side counterpart, the <a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-driven anomaly detection guide</a> for runtime detection that pairs with kernel isolation, and the <a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack supply chain attack postmortem</a> for the kind of supply-chain blast radius this feature reduces.</p>
<p>For the original sources, see the <a href="https://kubernetes.io/blog/2026/04/23/kubernetes-v1-36-userns-ga/">Kubernetes 1.36 user namespaces GA announcement</a>, the <a href="https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/">user namespaces concept documentation</a>, the <a href="https://kubernetes.io/docs/tasks/configure-pod-container/user-namespaces/">task documentation for enabling user namespaces on a pod</a>, and <a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/127-user-namespaces/README.md">KEP-127</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero Trust Microservices with Spring Security</a>. The auth-side counterpart to kernel-level isolation. Both layers together close the practical attack paths into a pod.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/migrate-from-ingress-nginx-kubernetes-retired">How to Migrate from Ingress NGINX After Kubernetes Retired It</a>. The other K8s 1.36 era change you cannot defer. Pair the migration windows.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-Driven Anomaly Detection for Security</a>. Runtime detection layered on top of the kernel isolation user namespaces provide.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack npm Supply Chain Attack 2026</a>. The kind of supply-chain compromise user namespaces reduce the blast radius of when the malicious code lands inside a container.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Claude Opus 5: What Breaks When You Migrate from Opus 4.8]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/claude-opus-5-migration-breaking-changes</link>
      <guid>https://www.rabinarayanpatra.com/blogs/claude-opus-5-migration-breaking-changes</guid>
      <pubDate>Sun, 26 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Claude Opus 5 turns thinking on by default and 400s if you disable it above high effort. Full migration checklist from Opus 4.8, with pricing and effort tuning.]]></description>
      <content:encoded><![CDATA[<p>Anthropic shipped Claude Opus 5 on 24 July, and the migration note is unusually short: change the model string. Pricing is flat, the context window is unchanged, and existing prompts carry over.</p>
<p>Then you read the two behavior changes and realize one of them fails silently.</p>
<p>I went through this on my own projects over the weekend. The model ID swap took thirty seconds. Working out why one route started returning truncated answers took considerably longer, and the answer turned out to be documented in a single sentence I had skimmed past. So here is the migration in the order it actually bites you.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-opus-5-migration-breaking-changes.svg" alt="Claude Opus 5 cover card showing the model ID swap from claude-opus-4-8 to claude-opus-5, alongside pricing of $5 and $25 per million tokens, a 1M token context window, and 128K max output"></p>
<h2 id="what-is-claude-opus-5-and-what-does-it-cost">What is Claude Opus 5 and what does it cost?</h2>
<p>Claude Opus 5 is Anthropic's current Opus-tier model, released 24 July 2026 with the API model ID <code>claude-opus-5</code>. It costs $5 per million input tokens and $25 per million output tokens, identical to Claude Opus 4.8. There is no long-context premium and no date suffix on the ID.</p>
<p>The specs that matter:</p>
<table>
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Model ID</td>
<td><code>claude-opus-5</code></td>
</tr>
<tr>
<td>Context window</td>
<td>1M tokens (default <strong>and</strong> maximum)</td>
</tr>
<tr>
<td>Max output</td>
<td>128K tokens</td>
</tr>
<tr>
<td>Input / output price</td>
<td>$5 / $25 per million tokens</td>
</tr>
<tr>
<td>Thinking</td>
<td>On by default</td>
</tr>
<tr>
<td>Effort levels</td>
<td><code>low</code>, <code>medium</code>, <code>high</code>, <code>xhigh</code>, <code>max</code></td>
</tr>
<tr>
<td>Prompt cache minimum</td>
<td>512 tokens</td>
</tr>
</tbody>
</table>
<p>That "default and maximum" detail on the context window is worth reading twice. There is no smaller context variant to opt into, which removes a config knob some teams were using to control cost.</p>
<p>On benchmarks, Anthropic reports Opus 5 landing within 0.5% of Claude Fable 5 on CursorBench 3.2 at half the price, roughly three times the next-best score on ARC-AGI 3, and about 1.5 times the next-best pass rate on Zapier's AutomationBench. Treat vendor benchmarks as vendor benchmarks. The claim I find more useful is the shape of the gains: deep reasoning, long agentic loops, and test-time compute scaling, meaning the model converts extra effort into better answers more reliably than earlier Opus versions did.</p>
<p>It is available on the Claude API as <code>claude-opus-5</code>, on Amazon Bedrock as <code>anthropic.claude-opus-5</code>, and on Google Cloud and Microsoft Foundry under the bare ID. Opus 4.8 stays available everywhere, so there is no forced cutover.</p>
<p>One operational detail that is easy to miss: Claude Opus 5 draws on a <strong>separate rate-limit bucket</strong> from the combined Opus 4.x pool. Moving traffic over does not free headroom on the old bucket and does not inherit it. Check your tier's Opus 5 limits before you shift real volume.</p>
<h2 id="why-does-thinking-being-on-by-default-break-your-max_tokens">Why does thinking being on by default break your max_tokens?</h2>
<p>Because <code>max_tokens</code> caps thinking plus reply text together, and on Opus 4.8 a request that omitted the <code>thinking</code> field produced no thinking at all. The same request on Opus 5 thinks. Your reply now shares a budget it used to own outright.</p>
<p>This is the change that cost me an afternoon. Nothing throws. The request returns HTTP 200, <code>stop_reason</code> comes back as <code>"max_tokens"</code>, and the answer is cut mid-sentence. If you are logging only errors, you will not see it.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-opus-5-migration-breaking-changes-max-tokens.svg" alt="Diagram comparing max_tokens usage: on Opus 4.8 the whole budget holds reply text, on Opus 5 thinking takes a share and pushes the reply past the limit, producing stop_reason max_tokens"></p>
<p>The wire format did not change. <code>thinking: {"type": "adaptive"}</code> is still valid and is exactly equivalent to the new default. What changed is what happens when you say nothing:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Opus 4.8: no thinking field means no thinking.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 4096 tokens was all reply text.</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-4-8</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">4096</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Opus 5: identical call, but thinking now runs and eats</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># into the same 4096. Long answers get truncated.</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">4096</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>You have two honest fixes. Raise <code>max_tokens</code> to leave room for both, which is what I did on every route where the answer length varies. Or keep the old behavior explicitly:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">4096</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    thinking</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">disabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    output_config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">effort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">medium</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>That second option comes with its own trap, which is the next section.</p>
<p>Audit target: every call site that never set a <code>thinking</code> field. Those are the ones that silently changed behavior. Routes that already passed <code>{"type": "adaptive"}</code> are unaffected.</p>
<h2 id="why-does-disabling-thinking-now-return-a-400-error">Why does disabling thinking now return a 400 error?</h2>
<p>Because on Claude Opus 5, <code>thinking: {"type": "disabled"}</code> is only accepted when effort is <code>high</code> or below. Pair it with <code>xhigh</code> or <code>max</code> and you get a 400. On Opus 4.8 the two settings were independent, so this is a genuine breaking change rather than a default shift.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-opus-5-migration-breaking-changes-effort-matrix.svg" alt="Matrix showing thinking adaptive is valid at every effort level, while thinking disabled is valid only at low, medium and high, returning 400 at xhigh and max"></p>
<p>The part that catches people is that <strong>validation runs per request</strong>. Effort and thinking are checked independently on every call. A conversation can run happily for twenty turns at <code>high</code> with thinking off, then fail the moment your code bumps effort to <code>xhigh</code> for a harder step. Earlier success in the same session buys you nothing.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 400 invalid_request_error on Claude Opus 5</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">4096</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    thinking</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">disabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    output_config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">effort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">xhigh</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">   # &#x3C;- rejected</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>Pick one of two resolutions. Keep thinking off and cap effort at <code>high</code>, which costs you the top two tiers. Or delete the <code>thinking</code> field and keep the effort tier, which costs tokens and buys back quality.</p>
<p>My advice is the second one, and not just for compliance. Anthropic documents two real failure modes with thinking disabled on Opus 5: the model occasionally writes a tool call into its visible text instead of emitting a <code>tool_use</code> block, and it can leak internal XML tags into the response. The first is nastier than it sounds. The turn completes normally, no error is raised, and the tool never runs. In an agentic loop that phantom call then sits in the history and skews later turns.</p>
<p>If a route genuinely must keep thinking off, the documented mitigations are counterintuitive enough to be worth stating plainly:</p>
<ul>
<li>Give the model permission to talk first: <em>"You may say a brief sentence before using a tool."</em> The tool-call-as-text failure appears to come from suppressing the preamble it wants to write.</li>
<li><strong>Delete</strong> any instruction telling it not to think or not to reason. That kind of rule makes tag leakage worse, not better.</li>
<li>Write the tag instruction generically. <em>"Do not include internal or system XML tags in your response"</em> works better than naming thinking tags explicitly.</li>
</ul>
<p>For most teams, running at <code>low</code> or <code>medium</code> effort with thinking on is cheaper and better behaved than disabling thinking at <code>high</code>.</p>
<h2 id="how-should-you-pick-an-effort-level-on-claude-opus-5">How should you pick an effort level on Claude Opus 5?</h2>
<p>Start at <code>high</code>, which is the default, then sweep in both directions against your own evals. Effort carries more weight on Opus 5 than on any earlier Opus, because the model converts extra effort into better output more reliably.</p>
<p>Anthropic's published starting points and the model's measured behavior pull in slightly different directions, and both are useful:</p>
<ul>
<li><strong>Start <code>xhigh</code> for coding and agentic work</strong>, <code>high</code> for other intelligence-sensitive workloads. <code>max</code> is the top tier for the deepest reasoning, though it can overthink simple tasks.</li>
<li><strong>Then sweep downward</strong>, because <code>low</code> and <code>medium</code> are unusually strong here. They deliver good quality at a fraction of the tokens and latency on plenty of workloads.</li>
</ul>
<p>Do run the sweep. Effort defaults inherited from Opus 4.8 or 4.7 are rarely the right setting on this model, and I would not trust a number carried over from a previous migration. If you kept effort notes from the <a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Opus 4.7 release</a>, re-run them rather than reusing them.</p>
<p>At <code>xhigh</code> or <code>max</code>, set a large <code>max_tokens</code> so the model has room to think and act across tool calls and subagents. Start at 64000 and tune down. And stream anything that large, or you will hit SDK HTTP timeouts before the model finishes:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">with</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">64000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    output_config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">effort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">max</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> as</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get_final_message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span></code></pre></figure>
<h2 id="what-new-api-features-ship-with-claude-opus-5">What new API features ship with Claude Opus 5?</h2>
<p>Three, and two of them are behind beta headers. None are required to migrate.</p>
<p><strong>Mid-conversation tool changes</strong> (beta <code>mid-conversation-tool-changes-2026-07-01</code>) let you add or remove tools between turns without invalidating the prompt cache. Previously the tool list was fixed for a conversation's lifetime and any edit re-billed the entire prefix, because tools render at position zero. You declare the tool up front with <code>"defer_loading": true</code>, then surface it later with a <code>tool_addition</code> block on a system message. This is the control-plane counterpart to tool search: tool search is for discovery, this is for when your application knows the tool set changed.</p>
<p><strong>Default fallbacks mode</strong> (beta <code>server-side-fallback-2026-07-01</code>) is the one I would turn on for everyone. Opus 5 ships with stricter cybersecurity safeguards, and its classifiers can decline a request. A decline is an HTTP 200 with <code>stop_reason: "refusal"</code>, not an error, so code that reads <code>response.content[0]</code> unconditionally breaks on it. The <code>fallbacks</code> parameter re-runs a declined request on another model server-side:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">beta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">16000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    betas</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">server-side-fallback-2026-07-01</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    fallbacks</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">default</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stop_reason</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">refusal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">    handle_refusal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stop_details</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>Prefer <code>"default"</code> over pinning a model. It routes by refusal category, and it saves you a migration the next time a pinned fallback gets deprecated. Cyber-category refusals route to Opus 4.8. Note this is Claude API only, so on Bedrock, Google Cloud, or Foundry you need the SDK's client-side fallback middleware instead.</p>
<p><strong>A lower prompt cache minimum</strong>, at 512 tokens, down from 1024 on Opus 4.8. No code change, but it is worth re-checking prompts you previously wrote off as too short to cache. Anything between 512 and 1024 tokens now creates cache entries for free.</p>
<h2 id="which-prompt-instructions-should-you-delete-after-migrating">Which prompt instructions should you delete after migrating?</h2>
<p>The verification ones. Claude Opus 5 verifies its own work without being asked, so instructions like <em>"include a final verification step"</em> or <em>"use a subagent to verify"</em> now cause over-verification rather than preventing errors.</p>
<p>This inverts a standard prompting rule. "Ask the model to double-check itself" is generally sound advice and it is wrong here. If you keep a shared prompt library, that needs a carve-out for this model rather than a global rule. The same applies to framework-level scaffolding. Separate verification passes carried over from earlier models are likely redundant now.</p>
<p>Three other behavior shifts are worth tuning for, all documented by Anthropic:</p>
<p><strong>Responses run longer.</strong> Both conversational output and files the model writes to disk. Lowering <code>effort</code> does not reliably shorten visible output, so this is a prompting fix, not a config one. A short conciseness instruction is the lever.</p>
<p><strong>It narrates progress more in agentic sessions.</strong> If you added scaffolding to force interim status updates ("after every three tool calls, summarize"), remove it. Opus 5 does this on its own, and the combination is noisy.</p>
<p><strong>It delegates to subagents more readily.</strong> This is a direction change worth flagging, because Opus 4.8 under-reached for subagents and needed prompting to delegate at all. If you added "delegate more" guidance for 4.8, take it out, and consider an explicit cap. Each subagent re-establishes context, re-explores, reports back, and then the coordinator re-reads the report. That multiplies both cost and latency.</p>
<p>It can also expand task scope, adding steps you did not request. A short scope-discipline instruction handles it: deliver what was asked at the scope intended, flag a concern in a sentence if the ask looks wrong, and keep going rather than quietly widening the work.</p>
<h2 id="should-you-migrate-to-claude-opus-5-today">Should you migrate to Claude Opus 5 today?</h2>
<p>If you are on Opus 4.8, yes, and the cost case makes it easy: identical pricing, better output. The two breaking changes are both mechanical and both findable with a grep.</p>
<p>Here is the checklist I actually used:</p>
<ol>
<li>Swap the model ID to <code>claude-opus-5</code>.</li>
<li>Grep for call sites with <strong>no</strong> <code>thinking</code> field. Raise <code>max_tokens</code> on each, or set <code>{"type": "disabled"}</code> deliberately.</li>
<li>Grep for <code>"disabled"</code> paired with <code>xhigh</code> or <code>max</code> effort. Fix both sides of the pair.</li>
<li>Re-run your effort sweep from scratch. Do not reuse 4.8 numbers.</li>
<li>Add a <code>stop_reason == "refusal"</code> branch before reading <code>content</code>, and turn on <code>fallbacks: "default"</code>.</li>
<li>Delete verification instructions and forced progress-update scaffolding from your prompts.</li>
<li>Confirm your rate-limit headroom on the new bucket before shifting volume.</li>
</ol>
<p>Steps 1 to 3 are required. The rest is the difference between a migration that works and one that works well.</p>
<p>The thing I keep coming back to is step 6. We spent two years writing prompts that compensated for models which did not check their own work, did not narrate progress, and would not delegate. Those instructions are now actively counterproductive. Migrating to a better model is turning out to be less about adding capability and more about deleting the scaffolding you built for the last one, which is a strange and slightly humbling way to upgrade.</p>
<p>For the full specification, see Anthropic's <a href="https://platform.claude.com/docs/en/about-claude/models/whats-new-opus-5">What's new in Claude Opus 5</a>, the <a href="https://www.anthropic.com/news/claude-opus-5">Claude Opus 5 announcement</a>, and the <a href="https://platform.claude.com/docs/en/about-claude/models/migration-guide">model migration guide</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Claude Opus 4.7: Pricing, Benchmarks &#x26; Breaking Changes</a>. The previous Opus migration, where adaptive thinking and the removal of sampling parameters first landed.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-mythos-project-glasswing">Claude Mythos and Project Glasswing: Anthropic's Locked-Down AI Model</a>. Where the Fable and Mythos tier sits relative to Opus, and why Opus 5 at half the price matters.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/best-claude-skills-for-developers-2026">10 Best Claude Skills for Developers in 2026</a>. Skills that pair well with the longer agentic runs Opus 5 is built for.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Migrate from Ingress NGINX After Kubernetes Retired It]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/migrate-from-ingress-nginx-kubernetes-retired</link>
      <guid>https://www.rabinarayanpatra.com/blogs/migrate-from-ingress-nginx-kubernetes-retired</guid>
      <pubDate>Thu, 23 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Step-by-step Ingress NGINX migration after the March 2026 retirement. Traefik drop-in, Gateway API with Cilium, and zero-downtime cutover playbook.]]></description>
      <content:encoded><![CDATA[<p>On March 24, 2026, Kubernetes SIG Network and the Security Response Committee retired Ingress NGINX. The GitHub repositories went read-only the same day. No more releases. No more bug fixes. No more security patches.</p>
<p>Roughly half the cloud-native estate depends on this controller. The retirement does not break anything immediately, but every new CVE in the NGINX core, Lua, OpenResty, or the controller code itself stays unpatched. My own staging cluster ran Ingress NGINX behind every service. I migrated it the weekend the announcement landed, and this post is the playbook I used: which replacement to pick, the Traefik drop-in path for the panicked migration, the Gateway API with Cilium path for the proper migration, and the zero-downtime cutover sequence.</p>
<h2 id="what-happened-to-ingress-nginx">What happened to Ingress NGINX?</h2>
<p>Ingress NGINX hit end of life on March 24, 2026, when Kubernetes SIG Network and the Security Response Committee formally retired the <code>kubernetes/ingress-nginx</code> project. The retirement was telegraphed in <a href="https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/">the November 2025 announcement on kubernetes.io</a>, confirmed in <a href="https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/">the January 2026 SRC statement</a>, and made final at the March 2026 retirement date.</p>
<p>What changed on that date:</p>
<ul>
<li>The GitHub repos became read-only.</li>
<li>The controller no longer receives bug fixes, feature changes, or CVE patches.</li>
<li>Helm charts and container images that already exist stay published, but new ones will not be cut.</li>
<li>The planned successor InGate was also abandoned. The community did not show up to maintain it.</li>
</ul>
<p>What did not change:</p>
<ul>
<li>Existing in-cluster Ingress NGINX deployments keep running.</li>
<li>Existing <code>Ingress</code> resources keep working against any controller that implements the Ingress API.</li>
<li>Cloud-managed alternatives (AWS ALB Controller, GCE Ingress, Azure Application Gateway) are unaffected because they are separate codebases.</li>
</ul>
<p>The official recommendation from SIG Network is direct: begin migration to Gateway API or to another actively maintained Ingress controller immediately. The Kubernetes blog's own words are that staying on Ingress NGINX after retirement "leaves you and your users vulnerable to attack". If you ship anything to production behind this controller, this is the patch you cannot defer.</p>
<h2 id="who-needs-to-migrate-and-how-urgent-is-it">Who needs to migrate and how urgent is it?</h2>
<p>Anyone running the <code>kubernetes/ingress-nginx</code> controller needs to migrate. The urgency depends on what kind of traffic the controller fronts. Public-facing clusters with any auth surface should treat this as a same-quarter migration. Internal-only clusters get one cycle of grace at most.</p>
<p>Use this matrix to scope your own work.</p>
<table>
<thead>
<tr>
<th>Cluster type</th>
<th>Traffic</th>
<th>Urgency</th>
<th>Suggested target</th>
</tr>
</thead>
<tbody>
<tr>
<td>Public production</td>
<td>Auth, payments, user data</td>
<td>Same quarter</td>
<td>Gateway API + Cilium / Istio / Envoy Gateway</td>
</tr>
<tr>
<td>Public production</td>
<td>Static or CDN-fronted only</td>
<td>One quarter</td>
<td>Traefik drop-in, then Gateway API</td>
</tr>
<tr>
<td>Internal staging</td>
<td>Dev traffic</td>
<td>One cycle</td>
<td>Traefik drop-in</td>
</tr>
<tr>
<td>Edge / IoT</td>
<td>Mixed</td>
<td>Same quarter</td>
<td>Gateway API + Cilium (if already on Cilium CNI)</td>
</tr>
<tr>
<td>Cloud-managed</td>
<td>Already on ALB/GCE/AGIC</td>
<td>None</td>
<td>No action</td>
</tr>
</tbody>
</table>
<p>The thing to watch on the urgent rows is your own CVE feed. Every Nginx core CVE, every OpenResty advisory, every Lua module disclosure now stays unpatched against the retired controller. A WAF in front of it buys you time but not safety, because most of the meaningful vulnerabilities are at the HTTP layer the WAF needs to let through.</p>
<p>I disagree with one piece of common advice making the rounds: that you can ride out the retirement with a strict WAF policy and pinned image hashes. That works for a few weeks. It does not work for a year. The same logic that retired the controller (limited maintainer bandwidth, growing CVE backlog) applies to the WAF policy too, because the WAF authors are not building new rules against a controller nobody else uses anymore.</p>
<h2 id="which-replacement-should-you-pick">Which replacement should you pick?</h2>
<p>Pick Gateway API as the long-term destination and Traefik as the safe staging point if you cannot reach Gateway API immediately. The community has aligned on Gateway API as the next-generation traffic management standard. Traefik is the only realistic drop-in replacement because its NGINX Ingress Provider reads existing <code>nginx.ingress.kubernetes.io</code> annotations natively.</p>
<p>Here is the short comparison.</p>
<table>
<thead>
<tr>
<th>Replacement</th>
<th>Drop-in for nginx annotations</th>
<th>Gateway API support</th>
<th>Where it fits</th>
</tr>
</thead>
<tbody>
<tr>
<td>Gateway API + Cilium</td>
<td>No (manifest rewrite)</td>
<td>Native, GA</td>
<td>Long-term standard. Best if already on Cilium CNI.</td>
</tr>
<tr>
<td>Gateway API + Istio</td>
<td>No (manifest rewrite)</td>
<td>Native, GA</td>
<td>Long-term standard. Best with existing service mesh.</td>
</tr>
<tr>
<td>Gateway API + Envoy Gateway</td>
<td>No (manifest rewrite)</td>
<td>Native, GA</td>
<td>Long-term standard. Minimal extra footprint.</td>
</tr>
<tr>
<td>Traefik</td>
<td>Yes (NGINX Ingress Provider)</td>
<td>Yes</td>
<td>Fastest cutover. Manifest changes optional.</td>
</tr>
<tr>
<td>HAProxy Ingress</td>
<td>Partial</td>
<td>Yes</td>
<td>Performance focus, HTTP/3 native.</td>
</tr>
<tr>
<td>Kong</td>
<td>Partial</td>
<td>Yes</td>
<td>If you also want an API gateway, not just ingress.</td>
</tr>
</tbody>
</table>
<p>For my own staging cluster I ran a two-phase migration: Traefik first, Gateway API second. The Traefik phase took two hours. The Gateway API phase took a week. Both shipped without downtime.</p>
<p>Cilium is the right Gateway API implementation if your cluster is already on Cilium CNI. The functionality is built in. You do not deploy a second component, and the eBPF datapath that already routes pod traffic also handles ingress.</p>
<h2 id="how-do-you-migrate-to-traefik-with-the-drop-in-adapter">How do you migrate to Traefik with the drop-in adapter?</h2>
<p>You migrate to Traefik by installing the chart with the NGINX Ingress Provider enabled, leaving Ingress NGINX running side by side, and switching your DNS or LoadBalancer one host at a time. Traefik reads your existing <code>Ingress</code> resources directly. You do not rewrite manifests in the first pass.</p>
<p>The minimum Helm install looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">helm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> repo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> add</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> traefik</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://traefik.github.io/charts</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">helm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> repo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> update</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">helm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> traefik</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> traefik/traefik</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --namespace</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> traefik-system</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --create-namespace</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> providers.kubernetesIngress.enabled=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> providers.kubernetesIngress.publishedService.enabled=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> providers.kubernetesIngress.ingressClass=traefik</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ingressClass.enabled=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ingressClass.isDefaultClass=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ingressRoute.dashboard.enabled=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">false</span></span></code></pre></figure>
<p>Two settings to call out. First, <code>providers.kubernetesIngress.enabled=true</code> turns on the NGINX-compatible Ingress reader. Second, <code>ingressClass.isDefaultClass=false</code> keeps Traefik from grabbing every existing <code>Ingress</code> resource at install time. You opt resources in one by one by changing their class.</p>
<p>Now tag the first Ingress to move.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># my-app-ingress.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> networking.k8s.io/v1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Ingress</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-app</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    kubernetes.io/ingress.class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> traefik</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">   # was: nginx</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    nginx.ingress.kubernetes.io/rewrite-target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  ingressClassName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> traefik</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">   # was: nginx</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  rules</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> host</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-app.example.com</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        paths</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">            pathType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Prefix</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">            backend</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">              service</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">                name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-app</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">                port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">                  number</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 80</span></span></code></pre></figure>
<p>The <code>nginx.ingress.kubernetes.io/rewrite-target</code> annotation stays. Traefik reads it. So do most of the popular annotations in that namespace, including <code>auth-url</code>, <code>auth-signin</code>, <code>enable-cors</code>, <code>force-ssl-redirect</code>, and <code>ssl-redirect</code>. The <a href="https://doc.traefik.io/traefik/providers/kubernetes-ingress/">Traefik documentation lists the supported set</a> and clearly marks the ones that are not handled. Read that page once before you start, because the annotations that do not translate cleanly tend to be the regex-based rewrites and the more exotic auth flows.</p>
<p>Apply the manifest. Now the resource is owned by Traefik. The Ingress NGINX controller stops reconciling it the moment the class flips.</p>
<p>Repeat for every Ingress in the cluster. When the last one is over, scale Ingress NGINX to zero.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> scale</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -n</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ingress-nginx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> deployment/ingress-nginx-controller</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --replicas=0</span></span></code></pre></figure>
<p>Leave it at zero for a week. If nothing breaks, delete the namespace. If something breaks, scale it back up and the failed resource resumes traffic the moment the class flips back.</p>
<h2 id="how-do-you-migrate-to-gateway-api-with-cilium">How do you migrate to Gateway API with Cilium?</h2>
<p>You migrate to Gateway API by installing the CRDs, enabling the Gateway API feature in your chosen implementation, declaring a <code>Gateway</code> resource per listener, then rewriting each <code>Ingress</code> as an <code>HTTPRoute</code>. The Cilium path is the most compact because Cilium already runs as your CNI and the Gateway API support is a flag, not a new component.</p>
<p>Install the standard Gateway API CRDs first.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> apply</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml</span></span></code></pre></figure>
<p>Then enable Gateway API in Cilium. For an existing Cilium install this is a Helm value update.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">helm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> upgrade</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> cilium</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> cilium/cilium</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --namespace</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kube-system</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --reuse-values</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gatewayAPI.enabled=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gatewayAPI.enableAlpn=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --set</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gatewayAPI.enableAppProtocol=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span></span></code></pre></figure>
<p>Restart the Cilium operator. The Gateway API controller comes up as part of the existing Cilium daemon set.</p>
<p>Declare the <code>Gateway</code>. This replaces what used to be the Ingress NGINX <code>LoadBalancer</code> Service plus the <code>Ingress</code> host blocks.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># gateway.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gateway.networking.k8s.io/v1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Gateway</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> public</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  namespace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gateway-system</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  gatewayClassName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> cilium</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  listeners</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 443</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      protocol</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> HTTPS</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      hostname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">*.example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      tls</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        mode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Terminate</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        certificateRefs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Secret</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">            name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> wildcard-example-com</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      allowedRoutes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        namespaces</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> All</span></span></code></pre></figure>
<p>Now rewrite the per-app Ingress as an <code>HTTPRoute</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># my-app-route.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gateway.networking.k8s.io/v1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> HTTPRoute</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-app</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  parentRefs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> public</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      namespace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gateway-system</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  hostnames</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-app.example.com</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  rules</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> matches</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">            type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> PathPrefix</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">            value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      backendRefs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-app</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 80</span></span></code></pre></figure>
<p>The <code>Ingress</code> resource and the <code>HTTPRoute</code> resource can coexist for the same host. Apply the route while the Ingress is still serving traffic. The Gateway API controller picks it up but does not become authoritative for the host until you redirect DNS or the LoadBalancer.</p>
<p>Three things the Gateway API does cleanly that Ingress did not. First, listener configuration (TLS, hostname, port) is a separate first-class resource, not a stack of annotations. Second, route attachment is cross-namespace by default, gated by <code>allowedRoutes</code>. Third, traffic-management features (weighted backends, header rewrites, request mirroring) are part of the spec, not vendor-specific annotations.</p>
<p>The Cilium-specific docs cover the eBPF datapath details and the Gateway API conformance matrix. The Cilium release notes confirm Gateway API GA in v1.15. If you are on a Cilium version older than that, upgrade Cilium first.</p>
<h2 id="how-do-you-cut-over-without-downtime">How do you cut over without downtime?</h2>
<p>You cut over without downtime by running the old and new controllers side by side, swinging traffic at the DNS or LoadBalancer layer rather than inside the cluster, and watching the new controller for one full traffic cycle before scaling the old one down. The wrong order is to delete the old controller first.</p>
<p>Here is the sequence I used on my own cluster.</p>
<ol>
<li>Install the new controller in its own namespace. Do not change any <code>Ingress</code> or <code>IngressClassName</code> yet.</li>
<li>Verify the new controller comes up healthy with a synthetic backend. A small <code>echo</code> deployment behind one test Ingress (or <code>HTTPRoute</code>) is enough.</li>
<li>For the first real workload, point a low-traffic DNS record at the new controller's LoadBalancer IP. Use a per-host record, not a wildcard, so the blast radius is bounded.</li>
<li>Watch for one full traffic cycle. At my scale that is 24 hours. For a high-traffic app it is one peak window.</li>
<li>If error rates and latency match the old controller, move the next host. Repeat.</li>
<li>When every host is on the new controller, drop the old LoadBalancer service and scale the controller to zero. Leave the deployment in place for a week as a rollback.</li>
<li>Delete the old namespace.</li>
</ol>
<p>The DNS-level swing is the part that matters. Changing <code>IngressClassName</code> on a live resource also works, but it cuts traffic instantly. A DNS swing lets you bleed traffic over the TTL window, and if something is wrong with the new controller the rollback is a single record change.</p>
<p>For services that depend on the LoadBalancer source IP for security policy, the cutover needs a coordinated step where you tell the upstream firewall about both LoadBalancer IPs for the duration of the migration. I learned this the loud way when a third-party webhook started returning 403 from one provider after I moved their host. The provider's allowlist still pointed at the old LoadBalancer.</p>
<h2 id="how-do-you-verify-the-migration">How do you verify the migration?</h2>
<p>You verify the migration by running the conformance suite for your new controller, replaying a representative sample of production traffic, and watching golden signals during a full traffic cycle. If any of those three reveals a regression, do not delete the old controller yet.</p>
<p>For Gateway API specifically, the conformance suite is the first signal.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> clone</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --depth=1</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://github.com/kubernetes-sigs/gateway-api</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">cd</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gateway-api/conformance</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">go</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> test</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ./...</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -args</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -gateway-class=cilium</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -conformance-profiles=GATEWAY-HTTP,GATEWAY-TLS</span></span></code></pre></figure>
<p>Every test that passes against your cluster certifies a piece of the spec that your config relies on. The full suite takes about ten minutes. Failures point at exactly which <code>HTTPRoute</code> feature your cluster does not handle.</p>
<p>For Traefik, the in-tree integration tests are the equivalent.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">kubectl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> logs</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -n</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> traefik-system</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> deploy/traefik</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">error|fail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>This is less rigorous than the Gateway API suite, but the Traefik controller is verbose about misconfigured Ingress resources. A clean log over 24 hours is the practical signal that your annotations translated correctly.</p>
<p>For traffic replay, the cleanest tool is <code>goldpinger</code> plus a TLS-aware HTTP probe.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># verify-route.yaml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> batch/v1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Job</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> verify-my-app</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  template</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    spec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      restartPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Never</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      containers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> probe</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> curlimages/curl:8.7.1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> sh</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> -c</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            -</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> |</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">              for i in $(seq 1 100); do</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                curl -fsS \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                  -H "Host: my-app.example.com" \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                  https://my-app.example.com/healthz \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                  || { echo "fail at $i"; exit 1; }</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                sleep 1</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">              done</span></span></code></pre></figure>
<p>Run that against both the old and new LoadBalancer IPs while traffic is still going through the old controller. A divergence shows up fast.</p>
<p>The final check is the golden signals. Watch p99 latency, 4xx rate, and 5xx rate on the affected hosts for one full traffic cycle. Compare against the day before the cutover, not against the day of, because the day of will show DNS-propagation artifacts.</p>
<p>For deeper context on related infrastructure shifts, see the <a href="https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide">Postgres connection pool pgbouncer survival guide</a> for the same pattern applied to database fronting, the <a href="https://www.rabinarayanpatra.com/blogs/ssh-tunnel-local-database-access">SSH tunnel local database access guide</a> for a non-cluster fallback when you need direct backend reach during the migration, and the <a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero trust microservices with Spring Security</a> for the auth-binding implications of the controller change.</p>
<p>For the original sources, see the <a href="https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/">Ingress NGINX retirement announcement</a>, the <a href="https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/">SRC statement on the retirement</a>, the <a href="https://traefik.io/blog/ingress-nginx-is-out">Traefik drop-in migration guide</a>, and the <a href="https://docs.cilium.io/en/stable/network/servicemesh/gateway-api/gateway-api/">Cilium Gateway API documentation</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide">Postgres Connection Pool: pgbouncer Survival Guide</a>. The same pattern of fronting a critical backend with the right adapter, applied to Postgres.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ssh-tunnel-local-database-access">SSH Tunnel for Local Database Access</a>. The non-cluster fallback you reach for when you need direct backend reach during a controller migration.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero Trust Microservices with Spring Security</a>. The auth-binding model that has to follow the controller through the migration.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[10 Best Claude Skills for Marketers in 2026]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/best-claude-skills-for-marketers-2026</link>
      <guid>https://www.rabinarayanpatra.com/blogs/best-claude-skills-for-marketers-2026</guid>
      <pubDate>Tue, 21 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[10 best Claude skills for marketers in 2026. Real picks for SEO, content research, brand work, and image gen. Direct links per skill and what each replaces.]]></description>
      <content:encoded><![CDATA[<p>Claude Skills landed in October 2025 and the marketing-focused ones have quietly replaced a stack of SaaS tools I used to pay for. SEO audits that needed Semrush, a full audit takes thirty seconds. Content briefs that needed Frase, the research-and-write skill spits them out with citations. Brand briefs that needed a deck, one command and a structured brand-guidelines doc is on disk.</p>
<p>Quick-look format below. Each skill links to its source folder. What it does. What I use it for. Ranked by impact in real marketing work; parent-repo star counts listed for transparency.</p>
<h2 id="what-are-claude-skills-for-marketers">What are Claude Skills for marketers?</h2>
<p>A Claude Skill is a folder with a <code>SKILL.md</code> file that teaches Claude one specific marketing task. When you ask for the task, Claude loads the skill, follows the playbook, and runs the workflow.</p>
<p>The payoff is consistency. Same audit format every time. Same brief shape every time. Same brand voice on every output. Skills make Claude reliable on the boring parts so your attention goes to the parts that need a brain.</p>
<h2 id="how-do-you-install-claude-skills-if-you-do-not-code">How do you install Claude Skills if you do not code?</h2>
<p>You need Claude Code installed (free from <a href="https://claude.com/code">claude.com/code</a>) or a claude.ai web account. Three install paths:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Path 1: Anthropic official skills</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">mkdir</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -p</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/.claude/skills</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">cd</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/.claude/skills</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> clone</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --depth</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://github.com/anthropics/skills.git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> anthropic-skills</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Path 2: SEO skill suite (25 sub-skills, MIT license)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> clone</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --depth</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://github.com/AgriciDaniel/claude-seo.git</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Path 3: community skills index</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># https://github.com/ComposioHQ/awesome-claude-skills</span></span></code></pre></figure>
<p>After install, restart your Claude Code session once. Skills auto-discover.</p>
<h2 id="what-are-the-10-best-claude-skills-for-marketers">What are the 10 best Claude skills for marketers?</h2>
<p>Ranked by impact in real marketing work. Parent-repo stars listed.</p>
<h3 id="1-seo-audit">1. seo-audit</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/AgriciDaniel/claude-seo/tree/main/skills/seo-audit">AgriciDaniel/claude-seo · seo-audit</a> (parent repo 7,194 stars)</li>
<li><strong>What it does:</strong> crawls up to 500 pages, detects business type, delegates to specialist sub-skills (technical, content, schema, images, sitemap, Core Web Vitals, GEO, backlinks, performance), generates a 0-100 health score.</li>
<li><strong>Use it for:</strong> any new client baseline. Replaces a multi-hour Screaming Frog + Lighthouse + schema-check sweep.</li>
</ul>
<h3 id="2-content-research-writer">2. content-research-writer</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/ComposioHQ/awesome-claude-skills/tree/master/content-research-writer">ComposioHQ/awesome-claude-skills · content-research-writer</a> (parent repo 62k stars)</li>
<li><strong>What it does:</strong> turns a topic into a researched, sourced, publication-ready markdown article. Multi-pass WebSearch, primary-source collection, outline, section-by-section drafting with live feedback.</li>
<li><strong>Use it for:</strong> any 1500+ word post where research is the bottleneck. Replaces Frase briefs at zero subscription cost.</li>
</ul>
<h3 id="3-seo-content">3. seo-content</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/AgriciDaniel/claude-seo/tree/main/skills/seo-content">AgriciDaniel/claude-seo · seo-content</a> (7,194 stars)</li>
<li><strong>What it does:</strong> scores a draft for E-E-A-T signals, readability, depth, thin-content patterns, first-person experience markers, and AI citation readiness.</li>
<li><strong>Use it for:</strong> pre-publish review. E-E-A-T is the biggest ranking factor in 2026 that most content tools cannot measure.</li>
</ul>
<h3 id="4-seo-geo">4. seo-geo</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/AgriciDaniel/claude-seo/tree/main/skills/seo-geo">AgriciDaniel/claude-seo · seo-geo</a> (7,194 stars)</li>
<li><strong>What it does:</strong> scores AI-crawler accessibility (GPTBot, ClaudeBot, PerplexityBot), llms.txt compliance, passage-level citability, and platform-specific optimization for AI Overviews, ChatGPT, Perplexity, and Bing Copilot.</li>
<li><strong>Use it for:</strong> any post you want cited by AI search. The 2026 evolution of traditional SEO.</li>
</ul>
<h3 id="5-seo-schema">5. seo-schema</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/AgriciDaniel/claude-seo/tree/main/skills/seo-schema">AgriciDaniel/claude-seo · seo-schema</a> (7,194 stars)</li>
<li><strong>What it does:</strong> detects, validates, and generates Schema.org structured data in JSON-LD. Article, Product, FAQPage, Breadcrumb, ItemList, and more.</li>
<li><strong>Use it for:</strong> any non-trivial page type (comparison, listicle, product, recipe). Schema is where rich snippets get won.</li>
</ul>
<h3 id="6-seo-page">6. seo-page</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/AgriciDaniel/claude-seo/tree/main/skills/seo-page">AgriciDaniel/claude-seo · seo-page</a> (7,194 stars)</li>
<li><strong>What it does:</strong> analyzes one URL across on-page elements, content quality, technical meta, schema, and performance. Per-URL scorecard with prioritized fixes.</li>
<li><strong>Use it for:</strong> the page that should be ranking but is not. Tells you what to fix in the order that matters.</li>
</ul>
<h3 id="7-brand-guidelines">7. brand-guidelines</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/anthropics/skills/tree/main/skills/brand-guidelines">anthropics/skills · brand-guidelines</a> (parent repo 142k stars)</li>
<li><strong>What it does:</strong> turns a few brand inputs (palette, voice, examples) into a structured brand-guidelines doc and applies it consistently to every downstream output.</li>
<li><strong>Use it for:</strong> any post, email, social caption, or asset that should sound like your brand. Replaces the "paste a tone-of-voice deck in every prompt" workaround.</li>
</ul>
<h3 id="8-web-artifacts-builder">8. web-artifacts-builder</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/anthropics/skills/tree/main/skills/web-artifacts-builder">anthropics/skills · web-artifacts-builder</a> (142k stars)</li>
<li><strong>What it does:</strong> builds elaborate self-contained HTML artifacts (multi-component React + Tailwind + shadcn/ui) for landing pages, comparison tools, calculators, lead magnets.</li>
<li><strong>Use it for:</strong> the one-pager you would have hired a freelancer for. Ships in an afternoon.</li>
</ul>
<h3 id="9-higgsfield-generate">9. higgsfield-generate</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/higgsfield-ai/skills/tree/main/higgsfield-generate">higgsfield-ai/skills · higgsfield-generate</a> (parent repo 318 stars)</li>
<li><strong>What it does:</strong> generates images via GPT Image 2 (technical diagrams), Nano Banana 2 (text-heavy), or Seedance (video). Marketing Studio mode handles ads with product imports + avatars.</li>
<li><strong>Use it for:</strong> cover images, social cards, marketing visuals. Replaces a small studio for ~$0.30 per image.</li>
</ul>
<h3 id="10-skill-creator">10. skill-creator</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/anthropics/skills/tree/main/skills/skill-creator">anthropics/skills · skill-creator</a> (142k stars)</li>
<li><strong>What it does:</strong> interviews you on trigger language, steps, and references, then writes a new <code>SKILL.md</code> in the right shape.</li>
<li><strong>Use it for:</strong> every repeatable marketing workflow (newsletter QA, link-build outreach, weekly report template). Build it once, reuse forever.</li>
</ul>
<h2 id="which-claude-skills-should-you-install-first">Which Claude skills should you install first?</h2>
<p>Install in this order:</p>
<ol>
<li>Install Claude Code from <a href="https://claude.com/code">claude.com/code</a>.</li>
<li>Clone <a href="https://github.com/AgriciDaniel/claude-seo">AgriciDaniel/claude-seo</a> into <code>~/.claude/skills/claude-seo/</code>. Gives you the full SEO suite (audit, page, content, geo, schema, plus 20 more sub-skills).</li>
<li>Clone <a href="https://github.com/anthropics/skills">anthropics/skills</a> into <code>~/.claude/skills/anthropic-skills/</code>. Gives you brand-guidelines, web-artifacts-builder, and skill-creator.</li>
<li>Add content-research-writer from <a href="https://github.com/ComposioHQ/awesome-claude-skills/tree/master/content-research-writer">ComposioHQ/awesome-claude-skills</a> for long-form research drafts.</li>
<li>Add <a href="https://github.com/higgsfield-ai/skills">higgsfield-ai/skills</a> when your content output exceeds your photographer or stock budget.</li>
</ol>
<p>The pattern that compounds: install one, use it for two weeks, install the next. The marketer who installs ten in one afternoon and uses none in week two saves zero hours. The one who installs one and uses it three times a week saves five hours by month two.</p>
<p>For more on building skills yourself, see the <a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills">Anthropic engineering deep-dive on agent skills</a>, the <a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview">Claude Skills documentation</a>, and the <a href="https://agentskills.io">open standard at agentskills.io</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects</a>. The mental model for skill vs MCP server choice.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-code-routines-ci-automation">Claude Code Routines: Async CI Automation Just Became Real</a>. Wire skills into scheduled tasks for hands-off content ops.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway Deep Dive</a>. One key for every LLM when your marketing stack needs to swap models.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Convert JSON to a Kotlin Data Class (2026 Guide)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-convert-json-to-kotlin-data-class</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-convert-json-to-kotlin-data-class</guid>
      <pubDate>Thu, 16 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Convert JSON to a Kotlin data class with kotlinx.serialization: snake_case with @SerialName, nullables, defaults, and nested objects. Plus an instant tool.]]></description>
      <content:encoded><![CDATA[<p>Converting JSON to a Kotlin data class looks like a five-minute job. Copy the keys, make a data class, done. Then the API returns <code>null</code> for a field you marked non-null, or the keys are <code>snake_case</code> and your properties are <code>camelCase</code>, and now you have a <code>SerializationException</code> in production.</p>
<p>I have written this mapping by hand more times than I can count, on Android apps and Ktor backends. This is the version that actually holds up: the right library, the annotations that matter, and the two or three things that bite everyone. And because typing out a 40-field class from a sample payload is soul-crushing, I will also show you the tool I built to skip that part entirely.</p>
<h2 id="how-do-you-convert-json-to-a-kotlin-data-class">How do you convert JSON to a Kotlin data class?</h2>
<p>You convert JSON to a Kotlin data class by defining a data class whose properties match the JSON keys, marking it <code>@Serializable</code>, and calling <code>Json.decodeFromString</code>. The official library for this is kotlinx.serialization, and it is the one I reach for on every new project.</p>
<p>Add the plugin and dependency to your Gradle build:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">plugins</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    kotlin</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"plugin.serialization"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) version </span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"2.1.0"</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">dependencies</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    implementation</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Then the class and the parse call:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> kotlinx.serialization.Serializable</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> kotlinx.serialization.json.Json</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">@Serializable</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> active: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Boolean</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> """{"id":1,"name":"Rabi","email":"r@example.com","active":true}"""</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Json.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decodeFromString</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">>(json)</span></span></code></pre></figure>
<p>That is the whole happy path. The <code>@Serializable</code> annotation triggers the compiler plugin to generate the serializer at build time, so there is no runtime reflection, which is why kotlinx.serialization works on Kotlin Multiplatform and Android without extra config. Leave the annotation off and you get a compile error, not a runtime one, which is the good kind of failure.</p>
<h2 id="why-kotlinxserialization-over-gson-or-moshi">Why kotlinx.serialization over Gson or Moshi?</h2>
<p>Because kotlinx.serialization is the official Kotlin library, it understands Kotlin types like non-null and default values natively, while Gson and Moshi were built for Java and fake it. If you are starting fresh, this is the one to pick.</p>
<p>Here is the practical difference. Gson uses reflection and will happily construct a Kotlin object with <code>null</code> in a non-null property, because it bypasses your constructors. That gives you a value that the Kotlin type system swears cannot be null, right up until it crashes somewhere far from the parse. Moshi with its Kotlin codegen fixes that, and it is a fine choice on existing Android projects that already use it. But it is another dependency and another annotation processor.</p>
<p>kotlinx.serialization sidesteps all of it. It respects nullability, it respects default values, and it fails loudly at parse time when the JSON does not match, instead of handing you a broken object. On a new Kotlin codebase I do not think the comparison is close. Reach for kotlinx.serialization and move on.</p>
<h2 id="how-do-you-map-snake_case-json-keys-to-kotlin-properties">How do you map snake_case JSON keys to Kotlin properties?</h2>
<p>Use the <code>@SerialName</code> annotation to bind a JSON key to a differently named Kotlin property. Most APIs return <code>snake_case</code>, and you do not want that leaking into idiomatic Kotlin code.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> kotlinx.serialization.SerialName</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> kotlinx.serialization.Serializable</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">@Serializable</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserProfile</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    @SerialName</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"full_name"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fullName: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    @SerialName</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"avatar_url"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> avatarUrl: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    @SerialName</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"created_at"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createdAt: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>Now the serializer reads <code>full_name</code> from the JSON but your code uses <code>fullName</code>. This is the single most common thing people get wrong, because without it the parse throws a <code>SerializationException</code> complaining about a missing field, and the field is right there in the JSON. It is not missing. Its name just does not match.</p>
<p>If literally every key is <code>snake_case</code>, there is a shortcut. Configure the <code>Json</code> instance with a naming strategy instead of annotating every property:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> kotlinx.serialization.json.Json</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> kotlinx.serialization.json.JsonNamingStrategy</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">@OptIn</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(kotlinx.serialization.ExperimentalSerializationApi::</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">class</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Json</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    namingStrategy </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> JsonNamingStrategy.SnakeCase</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>I still prefer explicit <code>@SerialName</code> on public API models, because it documents the exact wire contract at the property. But for an all-<code>snake_case</code> API, the global strategy saves a lot of noise.</p>
<h2 id="how-do-you-handle-nullable-fields-defaults-and-missing-keys">How do you handle nullable fields, defaults, and missing keys?</h2>
<p>Match the JSON's optionality in the Kotlin type: nullable properties for keys that can be <code>null</code>, default values for keys that can be absent. These are two different problems and people conflate them constantly.</p>
<p>A key that is present but can hold <code>null</code> needs a nullable type:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">@Serializable</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Account</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> nickname: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">?,        </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// present, but may be null</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> plan: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> "free"</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">     // may be absent entirely</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>The distinction that trips people up: a nullable type (<code>String?</code>) handles a key whose value is <code>null</code>. A default value handles a key that is missing from the JSON altogether. If a key might be absent and you do not give it a default, the parse fails. If it is present as <code>null</code> and you did not make it nullable, the parse also fails. They look similar and fail the same way, but the fix is different.</p>
<p>One more flag worth knowing. By default kotlinx.serialization rejects unknown keys, which is strict and safe. Real APIs add fields all the time, so on client models I usually relax that:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Json</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    ignoreUnknownKeys </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    coerceInputValues </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">   // treats null on a non-null with a default as the default</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>ignoreUnknownKeys</code> stops a new server field from breaking your app. <code>coerceInputValues</code> is the pragmatic escape hatch for APIs that send <code>null</code> where you expected a value with a default. Use them on client-side models. On server-side request bodies I keep strict parsing, because there I want the bad request rejected.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-convert-json-to-kotlin-data-class-mapping.svg" alt="Diagram mapping a JSON payload with snake_case keys and a nested object into a Kotlin data class using SerialName annotations and a nested data class"></p>
<h2 id="how-do-you-map-nested-objects-and-arrays">How do you map nested objects and arrays?</h2>
<p>Model each nested JSON object as its own <code>@Serializable</code> data class, and map arrays to <code>List</code>. kotlinx.serialization handles the nesting automatically once the types line up.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">@Serializable</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Address</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> city: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    @SerialName</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"zip_code"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> zipCode: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">@Serializable</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Customer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> address: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Address</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,          </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// nested object</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tags: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">List</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">>,        </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// array of primitives</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orders: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">List</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Order</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">>        </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// array of objects</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">@Serializable</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Order</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">val</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> total: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Double</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>The rule is simple: every JSON object becomes a data class, every JSON array becomes a <code>List</code> of whatever the elements are. There is no special config for depth. A five-level-deep payload just means five data classes, each <code>@Serializable</code>. This is also where doing it by hand stops being fun, and where a generator earns its keep.</p>
<h2 id="is-there-a-faster-way-than-writing-the-class-by-hand">Is there a faster way than writing the class by hand?</h2>
<p>Yes. Paste the JSON into a generator and it writes the entire data class for you, nested classes and all. This is exactly the tedium I got tired of, so I built <a href="https://jsontodto.com">jsontodto.com</a> to do it.</p>
<p>You drop in a sample payload and it produces the matching Kotlin data class, with nested objects split into separate classes and arrays typed as <code>List</code>. You can toggle a plain data class or add kotlinx.serialization decorators, and it infers types from the values. For a big third-party API response, this turns a 20-minute typing session into a paste. The tool also outputs Java records, TypeScript interfaces, Python dataclasses, Go structs, and C# records from the same JSON, so it is handy across a polyglot stack, not just Kotlin.</p>
<p>My honest workflow: generate the class from a real sample, then hand-tune it. The generator cannot know that a field is nullable if the sample happened to have a value there, and it cannot know which keys are optional across all responses. So I paste, generate, then go through and add <code>?</code> where the API can return null and defaults where keys can be absent. Generation gets you 90 percent of the way in one paste. The last 10 percent is the judgment this guide is about.</p>
<p>Converting JSON to a Kotlin data class is one of those tasks that is trivial until the real world shows up with <code>snake_case</code> keys, nullable everything, and objects nested five deep. Get the annotations right and kotlinx.serialization does the rest without complaint. Generate the boilerplate, then spend your actual attention on the nullability and optionality the generator cannot infer. That is where correctness lives.</p>
<p>For the full API, see the <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md">kotlinx.serialization JSON documentation</a> and the <a href="https://kotlinlang.org/docs/serialization.html">official Kotlin serialization guide</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/json-to-dto-converter">JSON to DTO Converter: Instantly Generate Java Classes from JSON</a>. The Java side of the same tool, for records, Lombok, and Jackson DTOs.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">Java Libraries Beyond Lombok</a>. More ways to cut boilerplate when you are back on the JVM side of a project.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Enable Virtual Threads in Spring Boot (Java 21+)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-enable-virtual-threads-spring-boot</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-enable-virtual-threads-spring-boot</guid>
      <pubDate>Tue, 14 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Enable virtual threads in Spring Boot with one property. What spring.threads.virtual.enabled wires up, the connection-pool trap, and when to skip it.]]></description>
      <content:encoded><![CDATA[<p>Enabling virtual threads in Spring Boot is genuinely one line. Set a property, run on Java 21, done. The flag is not where people get in trouble.</p>
<p>Where they get in trouble is thinking the flag is the whole story. It quietly rewires how your web server handles requests, which executor runs your <code>@Async</code> methods, and how your Kafka listeners get their threads. And it exposes a bottleneck that was always there but never mattered before. This is the guide I give teammates who are about to flip the switch in production. If you want the deeper theory on what virtual threads are and how Project Loom works, I covered that in <a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">my breakdown of virtual threads in Java 25</a>. Here I am focused on the Spring Boot side: what turning them on actually does, and what bites you.</p>
<h2 id="how-do-you-enable-virtual-threads-in-spring-boot">How do you enable virtual threads in Spring Boot?</h2>
<p>You enable virtual threads in Spring Boot by setting <code>spring.threads.virtual.enabled=true</code> and running on Java 21 or later. That is the entire setup.</p>
<p>In <code>application.properties</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="properties" data-theme="material-theme github-light"><code data-language="properties" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.threads.virtual.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">true</span></span></code></pre></figure>
<p>Or in <code>application.yml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  threads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    virtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span></code></pre></figure>
<p>This property has been available since Spring Boot 3.2, released in December 2023, and it carries forward unchanged into Spring Boot 4.x. The only hard requirement beyond the property is the runtime: virtual threads are a Java 21 feature, so on Java 17 or 20 the property does nothing useful.</p>
<p>I always pair it with an explicit check that the app is actually on 21+. A quick way is to log <code>Runtime.version()</code> at startup, or fail fast in a <code>@PostConstruct</code> if the major version is below 21. Silent no-ops are worse than crashes.</p>
<h2 id="what-does-springthreadsvirtualenabled-actually-turn-on">What does spring.threads.virtual.enabled actually turn on?</h2>
<p>The property flips several auto-configurations at once, not just the web server. Understanding the full set is the difference between "I enabled virtual threads" and "I know where my code runs now."</p>
<p>Here is what Spring Boot wires up when the flag is on and you are on Java 21+:</p>
<ul>
<li><strong>Web request handling.</strong> Tomcat and Jetty use virtual threads to process requests, so your controller methods run on a virtual thread per request instead of a pooled platform thread.</li>
<li><strong>The application task executor.</strong> The <code>applicationTaskExecutor</code> bean becomes a <code>SimpleAsyncTaskExecutor</code> that starts a new virtual thread per task. This is what backs <code>@Async</code> methods, Spring MVC async request processing, and WebFlux blocking execution support.</li>
<li><strong>The task scheduler.</strong> Scheduling is backed by a <code>SimpleAsyncTaskScheduler</code> on virtual threads, and it ignores pool-size properties because there is no pool to size.</li>
<li><strong>Messaging listeners.</strong> RabbitMQ and Kafka listener containers get virtual thread executors auto-configured.</li>
<li><strong>Data access helpers.</strong> Spring Data Redis' <code>ClusterCommandExecutor</code> runs on virtual threads.</li>
</ul>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-enable-virtual-threads-spring-boot-fanout.svg" alt="Diagram showing the single spring.threads.virtual.enabled property fanning out to Tomcat request threads, the application task executor, the task scheduler, and Kafka and RabbitMQ listeners"></p>
<p>The takeaway: this one property changes the execution model across the whole app, not just HTTP. If you have custom <code>Executor</code> beans wired by hand, they are not touched. Spring only swaps the executors it owns. So an app that defines its own <code>ThreadPoolTaskExecutor</code> for <code>@Async</code> keeps using platform threads there until you change it yourself.</p>
<h2 id="why-is-the-database-connection-pool-the-real-bottleneck">Why is the database connection pool the real bottleneck?</h2>
<p>Because virtual threads remove the thread limit but not the connection limit, so your JDBC pool becomes the ceiling the moment threads stop being scarce. This is the single most important thing to understand before enabling them.</p>
<p>The old model looked like this. Tomcat had 200 platform threads. Each request grabbed a thread, and if the thread pool was full, new requests queued. The thread pool was your concurrency limit, and it was usually smaller than your connection pool, so the pool rarely ran dry.</p>
<p>Flip on virtual threads and that changes. Now you can have 10,000 requests in flight, each on its own cheap virtual thread. But HikariCP still defaults to 10 database connections. So 10 requests run their query and the other 9,990 block waiting for a connection. You did not remove the wait. You moved it from "waiting for a thread" to "waiting for a connection," and the second one is easier to miss because everything looks healthy until latency spikes.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="properties" data-theme="material-theme github-light"><code data-language="properties" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Virtual threads let you accept far more concurrent requests.</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.threads.virtual.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">true</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># So the connection pool is now your real concurrency limit.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Size it against your database's capacity, not your request rate.</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.datasource.hikari.maximum-pool-size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">50</span></span></code></pre></figure>
<p>There is no magic number here. The right pool size is bounded by what your database can handle, and adding virtual threads does not change that. What changes is that the pool, not the thread count, is now the thing you tune. I have seen teams enable virtual threads, see no improvement, and conclude the feature is overhyped. The feature was fine. Their 10-connection pool was the wall.</p>
<h2 id="what-is-thread-pinning-and-when-should-you-worry-about-it">What is thread pinning and when should you worry about it?</h2>
<p>Thread pinning is when a virtual thread cannot unmount from its carrier platform thread during a blocking call, which cancels the scalability benefit for that operation. It used to be a real concern with <code>synchronized</code>, and it is mostly gone now.</p>
<p>On Java 21 through 23, a virtual thread that blocks inside a <code>synchronized</code> block stays pinned to its carrier thread. If that block wraps an I/O call, like a database query guarded by a synchronized method, you lose the whole point: the carrier thread is stuck, and you are back to a bounded number of real threads.</p>
<p>Java 24 fixed this. JEP 491 reworked synchronization so virtual threads no longer pin on <code>synchronized</code>, which removes the most common pinning source for most apps. So the advice depends on your runtime:</p>
<ul>
<li><strong>Java 24+:</strong> pinning from <code>synchronized</code> is no longer a concern. You can mostly stop worrying about it.</li>
<li><strong>Java 21 to 23:</strong> audit hot paths for <code>synchronized</code> around blocking I/O. Swap those specific locks to <code>ReentrantLock</code>, which lets the virtual thread unmount cleanly.</li>
</ul>
<p>You do not need to rip out every <code>synchronized</code> in your codebase. A lock held for a quick in-memory update does not pin in any way that matters. The one to hunt for is a lock held across a network or disk call. To find them, run with <code>-Djdk.tracePinnedThreads=full</code> on Java 21 to 23 and watch the logs under load.</p>
<h2 id="when-should-you-not-enable-virtual-threads">When should you not enable virtual threads?</h2>
<p>Skip virtual threads when your workload is CPU-bound, because they solve a blocking problem, not a compute problem. This is the clearest "no."</p>
<p>Virtual threads shine when threads spend most of their time waiting: database calls, HTTP calls to other services, file I/O. If your service does heavy computation, image processing, large in-memory transforms, number crunching, virtual threads add nothing. You still have the same number of CPU cores, and a virtual thread doing math occupies a carrier thread the whole time just like a platform thread would. For that work, a sized <code>ThreadPoolTaskExecutor</code> matched to your core count is still the right tool.</p>
<p>A few other cases where I hold off:</p>
<ul>
<li><strong>Heavy <code>ThreadLocal</code> usage.</strong> Each virtual thread carries its own <code>ThreadLocal</code> values. With millions of threads, a fat <code>ThreadLocal</code> cache turns into a memory problem. Audit what you stash there before scaling thread counts way up.</li>
<li><strong>Third-party libraries with internal thread pools.</strong> Enabling the Spring property does not change a library that manages its own executor. Check that your critical dependencies are Loom-friendly, not fighting it.</li>
<li><strong>You have not measured.</strong> If you do not know that thread starvation is your bottleneck, enabling virtual threads is a guess. Load test first, find the actual limit, then decide.</li>
</ul>
<p>Enabling virtual threads in Spring Boot is not a performance cheat code, and treating it like one is how you end up disappointed. It is a scalability tool for I/O-bound concurrency, and it pays off exactly when blocked threads were what held you back. Flip the property, then go re-tune your connection pool and re-run your load test. That second step is where the actual throughput lives.</p>
<p>For the official details, see the <a href="https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html">Spring Boot task execution and scheduling reference</a> and the <a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes">Spring Boot 3.2 release notes</a> where the property was introduced.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Virtual Threads in Java 25: The Complete Guide</a>. The language-level deep dive on how virtual threads and Project Loom work under the hood.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java with Spring Boot</a>. How the newer Java features fit into a real Spring Boot service.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Configure the Debezium Outbox Event Router SMT]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-configure-debezium-outbox-event-router</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-configure-debezium-outbox-event-router</guid>
      <pubDate>Thu, 09 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Configure the Debezium Outbox Event Router step by step: route to per-aggregate topics, set message keys, add header metadata, and expand JSON payloads.]]></description>
      <content:encoded><![CDATA[<p>The outbox pattern gets you a table full of events written in the same transaction as your business data. That solves the dual-write problem. But a Debezium connector pointed at that table emits raw row-change events, envelopes and all, on a single ugly topic. Nobody wants to consume that.</p>
<p>The piece that turns those raw row changes into clean, per-aggregate domain messages is the Outbox Event Router SMT. It is the single most useful transform Debezium ships, and it is also the one people misconfigure the most. This guide is the config reference I wish I had the first time I wired it up. If you need the pattern itself first (why the outbox table exists, how the transaction works), read <a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">my walkthrough of implementing the outbox pattern with CDC</a> and come back here for the routing details.</p>
<h2 id="what-does-the-debezium-outbox-event-router-actually-do">What does the Debezium Outbox Event Router actually do?</h2>
<p>The Debezium Outbox Event Router is a single message transform, class <code>io.debezium.transforms.outbox.EventRouter</code>, that reshapes raw outbox-table change events into clean messages on per-aggregate topics. It runs inside the Kafka Connect pipeline, after Debezium captures the insert and before the record hits Kafka.</p>
<p>Think of it as three jobs stacked together. It picks the destination topic from a column in the row. It sets the Kafka message key from another column. And it unwraps the event so the payload column becomes the message value instead of a nested change-event envelope.</p>
<p>By default it assumes your outbox table has these columns:</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Purpose</th>
<th>Default type</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>unique event id, used for dedup</td>
<td>uuid</td>
</tr>
<tr>
<td><code>aggregatetype</code></td>
<td>drives the topic name</td>
<td>varchar</td>
</tr>
<tr>
<td><code>aggregateid</code></td>
<td>becomes the Kafka message key</td>
<td>varchar</td>
</tr>
<tr>
<td><code>type</code></td>
<td>the event type (OrderCreated, etc.)</td>
<td>varchar</td>
</tr>
<tr>
<td><code>payload</code></td>
<td>the actual event body</td>
<td>json / jsonb</td>
</tr>
</tbody>
</table>
<p>You are not locked into those names. Every one of them is remappable. But if you control the table schema, matching the defaults means less config to get wrong.</p>
<h2 id="how-do-you-add-the-eventrouter-smt-to-a-connector">How do you add the EventRouter SMT to a connector?</h2>
<p>You add it as a named transform in the connector config, then set the transform type to the EventRouter class. Everything else is options prefixed with that transform name.</p>
<p>Here is the minimum viable version on a Postgres connector, assuming default column names:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">transforms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">outbox</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">transforms.outbox.type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">io.debezium.transforms.outbox.EventRouter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">table.include.list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">public.outbox</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That is genuinely all you need to start. With no other options, the SMT reads <code>aggregatetype</code>, routes to <code>outbox.event.&#x3C;aggregatetype></code>, keys the message by <code>aggregateid</code>, and uses <code>payload</code> as the value. The <code>table.include.list</code> is not part of the SMT, but you want it: without scoping the connector to the outbox table, Debezium captures every table in the schema and the transform errors on rows that do not look like outbox events.</p>
<p>One thing that bites people immediately. The EventRouter expects to see the row that was inserted, so it needs the new-record state. If you are also chaining <code>ExtractNewRecordState</code>, order matters. Put the outbox transform first, because it already handles the unwrap itself. Stacking a second flattening transform in front of it usually strips the envelope fields the router needs.</p>
<h2 id="how-do-you-route-events-to-per-aggregate-topics">How do you route events to per-aggregate topics?</h2>
<p>Routing is controlled by three options that work as a small pipeline: read a field, match it with a regex, substitute it into a template. The defaults route by aggregate type into a predictable topic name.</p>
<ul>
<li><code>route.by.field</code> (default <code>aggregatetype</code>): the column whose value drives the topic name.</li>
<li><code>route.topic.regex</code> (default <code>(?&#x3C;routedByValue>.*)</code>): a regex with a named capture group that pulls the routing value out.</li>
<li><code>route.topic.replacement</code> (default <code>outbox.event.${routedByValue}</code>): the topic name template. The capture group name is substituted in.</li>
</ul>
<p>So a row with <code>aggregatetype = Order</code> lands on <code>outbox.event.Order</code>. If you want a flatter naming scheme, override the replacement:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">transforms.outbox.route.by.field</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">aggregatetype</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">transforms.outbox.route.topic.replacement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">domain.${routedByValue}.events</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Now <code>Order</code> goes to <code>domain.Order.events</code>. The regex is worth knowing about for one real case: if your aggregate type values carry a prefix you do not want in the topic name, capture only the part you want. A <code>route.topic.regex</code> of <code>svc_(?&#x3C;routedByValue>.*)</code> turns <code>svc_Order</code> into a routing value of <code>Order</code>.</p>
<p>The message key is set separately through <code>table.field.event.key</code> (default <code>aggregateid</code>). This is the part that keeps per-entity ordering intact. Every event for the same order carries the same key, so Kafka lands them on the same partition and consumers see them in write order. If you leave the key column empty, you lose that guarantee, so treat <code>aggregateid</code> as required, not optional.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-configure-debezium-outbox-event-router-routing.svg" alt="Diagram showing an outbox table row flowing through the Debezium EventRouter SMT and fanning out into per-aggregate Kafka topics keyed by aggregate id"></p>
<h2 id="how-do-you-add-metadata-as-kafka-headers-or-envelope-fields">How do you add metadata as Kafka headers or envelope fields?</h2>
<p>You expose extra columns with <code>table.fields.additional.placement</code>, which takes a comma-separated list of <code>column:placement:alias</code> entries. The placement is either <code>header</code> or <code>envelope</code>, and that choice changes where consumers read the value.</p>
<p>Say your outbox table has an <code>eventtype</code> column and a <code>tracecontext</code> column you want to propagate. You want the event type inside the message value and the trace context as a Kafka header:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">transforms.outbox.table.fields.additional.placement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">    "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type:envelope:eventType,tracecontext:header:traceparent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Two rules I learned the hard way. <code>header</code> puts the value on the Kafka record header, which is perfect for cross-cutting metadata like trace ids that infrastructure reads without deserializing the body. <code>envelope</code> folds the value into the message value alongside the payload, which is what you want for domain fields a consumer actually maps into an object. Pick header for plumbing, envelope for data.</p>
<p>The alias (the third part) is the name the field or header gets on the output side. It is optional, but I always set it. Relying on the raw column name leaks your database schema into your event contract, and renaming the column later silently breaks every consumer.</p>
<h2 id="how-do-you-expand-the-json-payload-and-handle-deletes">How do you expand the JSON payload and handle deletes?</h2>
<p>Two options handle the payload shape and the delete case: <code>table.expand.json.payload</code> and <code>route.tombstone.on.empty.payload</code>. Both default to <code>false</code>, and both are safe to leave off until you need them.</p>
<p>By default the payload column is passed through as-is. If you stored it as a JSON string, consumers receive a string and have to parse it themselves. Turn on expansion to get a real structured record:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">transforms.outbox.table.expand.json.payload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>With that set, the SMT parses the payload into a proper Kafka Connect struct, so downstream schemas and converters see typed fields instead of one big string. The catch is that it only works when the payload is valid JSON. Malformed content makes the transform fail the record, so this pairs best with a <code>jsonb</code> column that the database already validates on write.</p>
<p>Deletes are the other edge. The outbox table is usually append-only, so you rarely delete rows. But if you do (say a cleanup job prunes old events), Debezium emits a delete change event. Turn on <code>route.tombstone.on.empty.payload</code> to convert those into proper Kafka tombstone records (a null value on the aggregate key), which is what log-compacted topics expect for key removal. If your outbox topics are not compacted, leave it off.</p>
<h2 id="what-does-a-complete-connector-configuration-look-like">What does a complete connector configuration look like?</h2>
<p>Here is a full Postgres connector config with the Event Router doing real work: custom routing, header metadata, JSON expansion, and message keying. This is close to what I have shipped in production.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">outbox-connector</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">connector.class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">io.debezium.connector.postgresql.PostgresConnector</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.hostname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">5432</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">debezium</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.dbname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">orders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">topic.prefix</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">orders-svc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">table.include.list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">public.outbox</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">tombstones.on.delete</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">outbox</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">io.debezium.transforms.outbox.EventRouter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.route.by.field</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">aggregatetype</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.route.topic.replacement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">domain.${routedByValue}.events</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.table.field.event.key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">aggregateid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.table.field.event.payload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">payload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.table.expand.json.payload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.table.fields.additional.placement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">      "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type:envelope:eventType,tracecontext:header:traceparent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>To verify it works, insert a row and watch the topic. A row with <code>aggregatetype = Order</code> and <code>aggregateid = 42</code> should produce a message on <code>domain.Order.events</code>, keyed by <code>42</code>, with the payload expanded into structured fields and a <code>traceparent</code> header attached. If the message lands on <code>outbox.event.Order</code> instead, your <code>route.topic.replacement</code> override is not being picked up, usually because the transform name in the property key does not match the one in the <code>transforms</code> list.</p>
<h2 id="what-are-the-common-pitfalls-with-the-event-router">What are the common pitfalls with the Event Router?</h2>
<p>Most Event Router problems trace back to a schema mismatch between the table and the config. Here are the ones I hit most, in rough order of how often they cost me an afternoon.</p>
<p>The connector captures more than the outbox table. Always set <code>table.include.list</code> to just the outbox table. Otherwise the transform sees regular business-table changes and throws because they lack <code>aggregatetype</code>.</p>
<p>The message key is null. Check that <code>table.field.event.key</code> points at a populated column. A null key destroys per-aggregate ordering, and it is easy to miss because messages still flow.</p>
<p>JSON expansion fails silently on bad data. If <code>table.expand.json.payload</code> is on and one row has a malformed payload, that record errors and can stall the connector. Validate at write time with a <code>jsonb</code> column, and consider a dead-letter queue on the connector.</p>
<p>Transform ordering with <code>ExtractNewRecordState</code>. The Event Router already unwraps the change event. Chaining a second flatten transform in front of it strips the fields it needs. If you must combine them, the outbox transform goes first.</p>
<p>Topic auto-creation is off. Per-aggregate routing means new aggregate types create new topics. If your broker disables auto topic creation, a brand new aggregate type produces messages that go nowhere until you create the topic. Pre-create them or enable Kafka Connect topic creation.</p>
<p>The Event Router is a small transform with a big payoff. Once the config matches your table, it disappears into the background and just produces clean domain events forever. The mistake I see teams make is treating it as an afterthought bolted onto the connector at the end. Design your outbox table columns around the router's defaults from day one, and the config shrinks to almost nothing.</p>
<p>For the full option list and version-specific behavior, see the <a href="https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html">Debezium Outbox Event Router documentation</a> and the <a href="https://github.com/debezium/debezium-examples/tree/main/outbox">Debezium outbox quickstart on GitHub</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">How to Implement the Outbox Pattern with CDC in Microservices</a>. Start here for why the outbox table exists and how the transactional write works before you configure the router.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero Trust Microservices with Spring Security</a>. Once your services talk over Kafka topics, this covers securing the service-to-service boundary.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Configure flake8 for Python (2026 Setup Guide)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-configure-flake8-python</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-configure-flake8-python</guid>
      <pubDate>Tue, 07 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Learn how to configure flake8 for Python: install it, set up .flake8, fix the pyproject.toml problem, wire up pre-commit, and decide if you still need it in 2026.]]></description>
      <content:encoded><![CDATA[<p>Every Python project I join eventually hits the same wall. Someone adds flake8, drops the config into <code>pyproject.toml</code> like every other modern tool expects, runs it, and nothing happens. No error, no warning, just flake8 quietly ignoring the entire file. Then the confusion starts.</p>
<p>This guide walks through how to configure flake8 properly in 2026: what it actually is, where its config really lives, why <code>pyproject.toml</code> is the one file it refuses to read, and whether you should still reach for it now that Ruff exists. Everything here is checked against flake8 7.3.0, released June 2025.</p>
<h2 id="what-is-flake8-and-what-does-it-actually-check">What is flake8 and what does it actually check?</h2>
<p>flake8 is not one linter. It is a wrapper that glues three separate tools together behind a single command, then adds a plugin system on top. When you run <code>flake8</code>, you are really running pyflakes, pycodestyle, and mccabe in one pass.</p>
<p>Each tool owns a code prefix, and knowing them makes every flake8 report readable:</p>
<ul>
<li><strong>pyflakes</strong> finds logical errors: unused imports, undefined names, unused variables. Its codes start with <strong>F</strong>.</li>
<li><strong>pycodestyle</strong> checks PEP 8 style: whitespace, indentation, line length. It was once called pep8. Its codes start with <strong>E</strong> (errors) and <strong>W</strong> (warnings).</li>
<li><strong>mccabe</strong> measures cyclomatic complexity. It emits exactly one code, <strong>C901</strong>, and only when you turn it on.</li>
</ul>
<p>flake8 adds one code of its own: <strong>E999</strong>, which means the file failed to compile at all (a syntax error). So when you see <code>F401</code>, that is pyflakes telling you about an unused import. <code>E501</code> is pycodestyle complaining about line length. <code>C901</code> is mccabe saying a function is too complex.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-configure-flake8-python-cover.svg" alt="flake8 glues pyflakes, pycodestyle, and mccabe into one command, each contributing its own error-code prefix"></p>
<p>flake8 7.3.0 pins those dependencies tightly: pyflakes 3.4.x, pycodestyle 2.14.x, and mccabe 0.7.x. It runs on Python 3.9 and up. The project lives under PyCQA (the Python Code Quality Authority) and is maintained by Anthony Sottile and Ian Cordasco.</p>
<h2 id="how-do-you-install-and-run-flake8">How do you install and run flake8?</h2>
<p>Install it with pip and point it at your code. That is the whole starting workflow:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">pip</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> flake8</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">flake8</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .</span></span></code></pre></figure>
<p>That runs flake8 across the current directory. You can target a specific path or module too:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">flake8</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> src/</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">python</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -m</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> flake8</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> src/</span></span></code></pre></figure>
<p>The <code>python -m flake8</code> form is worth remembering. It guarantees you run the flake8 installed in the current environment, which matters the moment you have more than one Python around. On a fresh run with no config, flake8 uses its defaults: 79-character lines and a small built-in ignore list. Most teams change both, which is where config comes in.</p>
<h2 id="where-does-flake8-look-for-its-configuration">Where does flake8 look for its configuration?</h2>
<p>flake8 reads three config files, all in INI format under a <code>[flake8]</code> section: <code>setup.cfg</code>, <code>tox.ini</code>, and a dedicated <code>.flake8</code> file. It does not read anything else, and it does not merge them. It picks one.</p>
<p>I default to a standalone <code>.flake8</code> file because it keeps linter config out of files that mean other things. Here is a realistic starting point, close to what the official docs recommend:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ini" data-theme="material-theme github-light"><code data-language="ini" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[flake8]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">max-line-length</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 88</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">extend-ignore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> E203</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">exclude</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> .git,__pycache__,docs/source/conf.py,old,build,dist</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">max-complexity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 10</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">per-file-ignores</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    __init__.py: F401</span></span></code></pre></figure>
<p>A few of those options carry sharp edges, so here is what each one actually does:</p>
<ul>
<li><strong><code>max-line-length</code></strong> defaults to 79. Setting it to 88 matches Black, which is why most projects use that number.</li>
<li><strong><code>extend-ignore</code></strong> adds codes to the ignore list without wiping the defaults. <code>ignore</code> (no <code>extend-</code>) replaces the entire default list, which usually is not what you want. Reach for the <code>extend-</code> forms.</li>
<li><strong><code>extend-select</code></strong> works the same way for turning checks on. It was added in flake8 4.0.0.</li>
<li><strong><code>exclude</code></strong> takes comma-separated paths and globs that flake8 skips entirely.</li>
<li><strong><code>per-file-ignores</code></strong> lets you relax rules per file. The <code>__init__.py: F401</code> line above silences unused-import warnings in package init files, where re-exporting is normal.</li>
<li><strong><code>max-complexity</code></strong> turns on the mccabe check. It is off until you set it, so <code>C901</code> never fires until you add a line like <code>max-complexity = 10</code>.</li>
</ul>
<p>The <code>extend-ignore = E203</code> in that example is there for a reason. E203 flags whitespace before a colon, which collides with how Black formats slices. Turning it off is the standard fix for running flake8 and Black together.</p>
<h2 id="why-wont-flake8-read-your-pyprojecttoml">Why won't flake8 read your pyproject.toml?</h2>
<p>flake8 does not read <code>pyproject.toml</code>, and that is a deliberate decision, not a missing feature. This is the single most common flake8 problem I see, and the answer surprises people: the maintainer has kept the request open for years and does not intend to add it as-is.</p>
<p>The relevant thread is <a href="https://github.com/PyCQA/flake8/issues/234">PyCQA/flake8 issue #234</a>, and it is still open. Anthony Sottile's position is direct: "I don't think toml is the way especially with pip continuing to have weird behaviour when the file even exists." His technical objection is real. The mere presence of a <code>pyproject.toml</code> file changes how pip builds a package, because it can force an isolated PEP 517 build. He listed two conditions that would need to hold before he would reconsider: pip no longer changing its behavior based on the file existing, and a standard-library TOML parser. Python 3.11 added <code>tomllib</code>, so half of that is now true, but flake8 7.3.0 still will not read the file.</p>
<p>So what do you do if your project standardizes on <code>pyproject.toml</code> and you want flake8 config to live there too? Use a plugin.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">pip</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Flake8-pyproject</span></span></code></pre></figure>
<p><a href="https://pypi.org/project/Flake8-pyproject/">Flake8-pyproject</a> lets you put your config under a <code>[tool.flake8]</code> section and either keeps working with the normal <code>flake8</code> command or gives you a <code>flake8p</code> entry point:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="toml" data-theme="material-theme github-light"><code data-language="toml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">tool</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">flake8</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">max-line-length </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 88</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">extend-ignore </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">E203</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">exclude </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">.git</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">__pycache__</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">dist</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span></code></pre></figure>
<p>There is also <code>pyproject-flake8</code> (the <code>pflake8</code> command), which monkey-patches flake8 to read the file. Both work. I lean toward Flake8-pyproject because it is the lighter touch, but understand that either one is a workaround layered on top of a tool that, by design, still does not natively support the format.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-configure-flake8-python-config-resolution.svg" alt="flake8 reads setup.cfg, tox.ini, and .flake8 but not pyproject.toml, which needs the Flake8-pyproject plugin as a bridge"></p>
<h2 id="how-do-you-silence-a-specific-warning-inline">How do you silence a specific warning inline?</h2>
<p>Add a <code># noqa</code> comment to the line, and be specific about which code you are silencing. flake8 gives you two forms, and the difference matters.</p>
<p>A bare <code># noqa</code> silences every error on that line:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> os  </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># noqa</span></span></code></pre></figure>
<p>A coded <code># noqa: E501</code> silences only that check. Anything else on the line still gets reported, which is exactly what you want:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">url </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://example.com/a/very/long/path/that/breaks/the/line/limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  # noqa: E501</span></span></code></pre></figure>
<p>You can list several codes with commas, like <code># noqa: E731,E123</code>, and the comment is case-insensitive. I always prefer the coded form. A bare <code># noqa</code> is a blunt instrument that hides real bugs the day someone adds a second problem to that line. If you ever need to audit what your <code># noqa</code> comments are actually hiding, run with <code>--disable-noqa</code> and flake8 reports everything as if the comments were not there.</p>
<h2 id="how-do-you-run-flake8-in-pre-commit-and-ci">How do you run flake8 in pre-commit and CI?</h2>
<p>Wire flake8 into pre-commit so it runs before code ever reaches a branch. The official hook is published in the flake8 repo, and the setup is a few lines in <code>.pre-commit-config.yaml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">-</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">   repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://github.com/pycqa/flake8</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    rev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 7.3.0</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    hooks</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">   id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> flake8</span></span></code></pre></figure>
<p>The docs show <code>rev: ''</code> as a placeholder, but always pin it to a real tag like <code>7.3.0</code>. An unpinned linter version means your CI can start failing on a Tuesday because a new release added a check, and nobody changed a line of code. For CI itself, there is nothing special to configure. The same <code>flake8 .</code> command that runs locally runs in a GitHub Actions step, reads the same config file, and exits non-zero on any violation. If you are hardening a pipeline, the same discipline I described in <a href="https://www.rabinarayanpatra.com/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc">enabling npm trusted publishing with OIDC</a> applies here: pin your tool versions and let the config file be the single source of truth.</p>
<h2 id="which-flake8-plugins-are-worth-adding">Which flake8 plugins are worth adding?</h2>
<p>Plugins extend flake8 with new checks under their own code prefixes, and installing one is just a pip install into the same environment. Most start reporting immediately, no config required. You can confirm what loaded by running <code>flake8 --version</code>, which lists every active plugin.</p>
<p>The ones I reach for most:</p>
<ul>
<li><strong>flake8-bugbear</strong> (prefix <code>B</code>) catches likely bugs and questionable design that pyflakes misses, like mutable default arguments.</li>
<li><strong>flake8-comprehensions</strong> (<code>C4</code>) flags list and dict comprehensions that could be simpler.</li>
<li><strong>flake8-docstrings</strong> (<code>D</code>) checks PEP 257 docstring conventions by wrapping pydocstyle.</li>
<li><strong>pep8-naming</strong> (<code>N</code>) enforces naming conventions for classes, functions, and variables.</li>
<li><strong>flake8-bandit</strong> adds security checks, and <strong>flake8-comprehensions</strong> and <strong>flake8-simplify</strong> clean up common code smells.</li>
</ul>
<p>Once a plugin is installed, its codes behave like any other. Turn them on or off with <code>select</code>, <code>ignore</code>, <code>extend-select</code>, and <code>extend-ignore</code> in your config. That is the real strength of flake8: the checker set is yours to compose.</p>
<h2 id="should-you-still-use-flake8-in-2026">Should you still use flake8 in 2026?</h2>
<p>Use flake8 if you depend on a plugin Ruff has not reimplemented. Otherwise, Ruff is probably the better default. That is the honest answer, and it comes straight from Ruff's own documentation rather than my opinion.</p>
<p><a href="https://docs.astral.sh/ruff/">Ruff</a> is a linter written in Rust that advertises being "10-100x faster than existing linters (like Flake8)." Its FAQ is refreshingly precise about compatibility. Ruff is a drop-in replacement for flake8 when you use it without many plugins, alongside Black, on Python 3 code. Under those conditions it "implements every rule in Flake8," including all the pyflakes <code>F</code> rules and a large subset of the <code>E</code> and <code>W</code> style rules.</p>
<p>But Ruff names its own limit clearly: "Ruff's primary limitation vis-a-vis Flake8 is that it does not support custom lint rules." That is the line that keeps flake8 alive. If your team relies on an internal flake8 plugin, or a niche community one Ruff has not ported, flake8 is still the tool that can run it. The same goes for a stable, working setup where migration churn buys you nothing. Speed is real, but a linter you have already tuned and trust has value too.</p>
<p>I have migrated projects to Ruff and kept others on flake8, and both calls were right for their context. The mistake is treating it as a moral question. Pick the tool that runs the checks you need, and let the benchmark difference decide only when the checks are equal.</p>
<p>For the details behind everything here, see the <a href="https://flake8.pycqa.org/en/latest/">flake8 documentation</a>, the still-open <a href="https://github.com/PyCQA/flake8/issues/234">pyproject.toml discussion in issue #234</a>, and <a href="https://docs.astral.sh/ruff/faq/">Ruff's FAQ</a> on flake8 compatibility.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-to-enable-npm-trusted-publishing-github-actions-oidc">How to Enable npm Trusted Publishing with GitHub Actions OIDC</a>. Another piece of a hardened CI pipeline, where pinned tool versions matter just as much.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/actions-checkout-v7-safe-pull-request-target">How to Check Out Fork PRs Safely in actions/checkout v7</a>. Running linters on untrusted pull requests without opening a security hole.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/serve-react-spa-fastapi-app-frontend">How to Serve a React SPA from FastAPI with app.frontend()</a>. More practical Python tooling, this time on the web-serving side.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[HTTP QUERY Method: The Safe GET With a Body (RFC 10008)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/http-query-method-rfc-10008</link>
      <guid>https://www.rabinarayanpatra.com/blogs/http-query-method-rfc-10008</guid>
      <pubDate>Thu, 02 Jul 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[The HTTP QUERY method (RFC 10008) gives you a safe, idempotent, cacheable request with a body. What it fixes, how caching works, and who supports it today.]]></description>
      <content:encoded><![CDATA[<p>Every REST API I have ever worked on grows a <code>POST /search</code> endpoint sooner or later. Not because POST is the right method for reading data, but because GET cannot carry a request body and the filter JSON stopped fitting in the URL years ago. As of mid-June 2026, HTTP finally has a proper fix. It is called QUERY, and it just became <a href="https://www.rfc-editor.org/rfc/rfc10008.html">RFC 10008</a>.</p>
<p>The HTTP QUERY method gives you the thing GET and POST each refuse to: a safe, idempotent, cacheable request that carries a body. In this post I will walk through what the spec actually says, why GET-with-a-body and POST-for-reads are both broken, how caching works when the cache key lives in the request body, and which runtimes you can use it on today.</p>
<h2 id="what-is-the-http-query-method">What is the HTTP QUERY method?</h2>
<p>The HTTP QUERY method is a new request method, standardized in RFC 10008 in mid-June 2026, that sends a query as the request body while keeping the guarantees of GET. The spec's abstract puts it well: "A QUERY requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing."</p>
<p>The body is the query. Not the resource, not a command, a question. "The content of the request and its media type define the query." That media type matters: the spec says servers MUST fail the request if <code>Content-Type</code> is missing or inconsistent with the content. A successful query comes back as a plain <code>200 OK</code> with the results in the response body.</p>
<p>Here is the comparison table straight from Section 1 of the RFC:</p>
<table>
<thead>
<tr>
<th>Property</th>
<th>GET</th>
<th>QUERY</th>
<th>POST</th>
</tr>
</thead>
<tbody>
<tr>
<td>Safe</td>
<td>Yes</td>
<td>Yes</td>
<td>Potentially no</td>
</tr>
<tr>
<td>Idempotent</td>
<td>Yes</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Cacheable</td>
<td>Yes</td>
<td>Yes</td>
<td>Only for future GET or HEAD requests</td>
</tr>
<tr>
<td>Request body</td>
<td>No defined semantics</td>
<td>Expected</td>
<td>Expected</td>
</tr>
</tbody>
</table>
<p>There is also a discovery mechanism. A server can advertise QUERY support with the new <code>Accept-Query</code> response header, a Structured Field listing the query media types it accepts for that path:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="http" data-theme="material-theme github-light"><code data-language="http" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Accept-Query</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> "application/jsonpath", application/sql;charset="UTF-8"</span></span></code></pre></figure>
<p>The RFC was written by Julian Reschke (greenbytes), James M. Snell (Cloudflare), and Mike Bishop (Akamai), and it is a Proposed Standard on the IETF standards track. This is not a vendor experiment. It is the real thing.</p>
<h2 id="why-isnt-get-with-a-body-enough">Why isn't GET with a body enough?</h2>
<p>Sending a body with GET is undefined behavior that some servers and intermediaries will actively reject. RFC 9110, the core HTTP semantics spec, is blunt about it in Section 9.3.1: "content received in a GET request has no generally defined semantics, cannot alter the meaning or target of the request, and might lead some implementations to reject the request and close the connection because of its potential as a request smuggling attack."</p>
<p>So the standard advice has always been: put the query in the URL. That works until it does not. RFC 9110 only recommends that implementations support URIs of at least 8,000 octets, and that is a floor, not a promise. Real limits vary per proxy, per load balancer, per server, and you discover the smallest one in the chain at runtime. The spec for the 414 status code even names the workaround as the culprit: it happens "when a client has improperly converted a POST request to a GET request with long query information."</p>
<p>I have debugged exactly one 414 in my career and it cost me most of a day, because the request worked fine locally and only failed behind a corporate proxy with a tighter URI limit. That is the nature of this failure: it is environmental, and your test suite will not catch it.</p>
<p>RFC 10008's introduction lists two more problems with query-in-the-URL that I find underrated. First, URLs leak: "request URIs are more likely to be logged than request content and may also turn up in bookmarks." If your query contains anything sensitive, it is now in every access log along the path. Second, my favorite line in the whole spec: encoding queries into the URI "effectively casts every possible combination of query inputs as distinct resources." Your cache and your metrics now treat <code>?city=Berlin&#x26;limit=50</code> and <code>?limit=50&#x26;city=Berlin</code> as different things, even though they are the same question.</p>
<h2 id="why-not-just-use-post-for-queries">Why not just use POST for queries?</h2>
<p>POST works for queries right up until you need retries, caching, or honest semantics. The problem is not that POST fails. It is that POST tells every piece of HTTP infrastructure to assume the worst.</p>
<p>POST is not safe and not idempotent. When a connection drops mid-request, neither the client nor any proxy can know whether state changed on the server. So nothing retries automatically. Compare that with the QUERY abstract: QUERY requests "can be automatically repeated or restarted without concern for partial state changes." For genuinely unsafe operations you end up building retry safety by hand, which is exactly the machinery I covered in <a href="https://www.rabinarayanpatra.com/blogs/implement-idempotency-keys-rest-api">idempotency keys for REST APIs</a>. Reads should never have needed that machinery in the first place.</p>
<p>And then there is caching. RFC 9110 Section 9.3.3 allows caching POST responses only when the response carries explicit freshness information plus a <code>Content-Location</code> that matches the target URI, and even then the cached response can only serve future GET and HEAD requests. It says directly: "a POST request cannot be satisfied by a cached POST response because POST is potentially unsafe." Two identical <code>POST /search</code> calls always hit your origin. Every dashboard that polls a search endpoint is doing full-price origin work on every tick.</p>
<p>QUERY flips all three defaults. Safe, so infrastructure can retry it. Idempotent, so retries need no bookkeeping. Cacheable, so identical questions can be answered without recomputing.</p>
<h2 id="how-does-caching-work-for-query-requests">How does caching work for QUERY requests?</h2>
<p>A QUERY response is cacheable, but the cache key must include the request content, not just the URI. That is the core trade the spec makes, and Section 2.7 is explicit: the cache key "MUST incorporate the request content" and related metadata.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/http-query-method-rfc-10008-cache-key.svg" alt="Diagram of a QUERY request cache key built from the method, target URI, and normalized request body, showing a cache miss followed by a cache hit"></p>
<p>This is genuinely harder than caching GET. The spec admits it: "caching QUERY method responses is inherently more complex than caching responses to GET, as complete reading of the request's content is needed in order to determine the cache key." A cache has to buffer the body before it can even decide whether it has seen this request before.</p>
<p>To avoid trivial misses, caches are allowed to normalize: they "MAY remove semantically insignificant differences from request content." The spec calls out stripping content encodings and using media type suffix knowledge, like treating anything ending in <code>+json</code> as JSON for normalization purposes. The flip side lands in the security section: normalize too aggressively and "normalization results in a false positive," meaning a cache can hand back the wrong query's results. If you build or configure a QUERY-aware cache, that is the failure mode to fear.</p>
<p>Conditional requests work too, through a clever indirection the spec calls the equivalent resource: a hypothetical GET-able resource derived by incorporating the request content into the target. Your <code>ETag</code> and <code>If-None-Match</code> validators operate against that, so a repeated query can come back <code>304 Not Modified</code>.</p>
<p>Two response headers round out the model, and they are easy to confuse:</p>
<ul>
<li><code>Content-Location</code> points at the query results, a URI you can plainly GET later to retrieve the same representation.</li>
<li><code>Location</code> points at the query itself, a URI that re-runs the query on GET without resending the body.</li>
</ul>
<p>One redirect trap worth knowing: the historical browser behavior of rewriting a redirected POST into a GET does not apply here. The spec states the 301/302 exceptions for POST do not apply to QUERY. Only a 303 tells the client to fetch the result with GET.</p>
<h2 id="why-was-it-named-query-instead-of-search">Why was it named QUERY instead of SEARCH?</h2>
<p>The spec spent its first six years named SEARCH and dropped the name to escape WebDAV baggage. The idea goes back to a 2015 individual draft (draft-snell-search-method), got revived by Asbjorn Ulsberg at the HTTP Workshop in 2019, and was adopted by the httpbis working group in March 2021, still called SEARCH.</p>
<p>The rename happened in November 2021. Appendix B of the RFC explains the reasoning with unusual candor. The IANA method registry already had three safe, idempotent methods: PROPFIND, REPORT, and SEARCH. All three come out of the WebDAV effort, "about which many have mixed feelings," and all use generic XML request formats. QUERY got a clean start, and as the appendix notes, the name "captures the relation with the URI's query component well."</p>
<p>From there it was a slow march: final draft in November 2025, IESG approval on November 20, 2025, and publication as RFC 10008 in mid-June 2026. Eleven years from first draft to standard. HTTP does not move fast, which is exactly why it is worth paying attention when it does move.</p>
<h2 id="who-supports-the-http-query-method-in-2026">Who supports the HTTP QUERY method in 2026?</h2>
<p>Node.js has parsed QUERY natively since early 2024, OpenAPI 3.2 can document it, and Spring's support is an open pull request. Here is the honest state of things as of July 2026:</p>
<table>
<thead>
<tr>
<th>Stack</th>
<th>Status (July 2026)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Node.js</td>
<td>Native. QUERY parsing landed via llhttp 9.2.0 in Node 21.7.2 and Node 22+</td>
</tr>
<tr>
<td>Express</td>
<td>Works on QUERY-capable Node, but no TypeScript types yet</td>
</tr>
<tr>
<td>Fastify</td>
<td>Opt-in: <code>fastify.addHttpMethod('QUERY', { hasBody: true })</code></td>
</tr>
<tr>
<td>Go net/http</td>
<td>No <code>MethodQuery</code> constant, but arbitrary method strings work today</td>
</tr>
<tr>
<td>Spring Framework</td>
<td>Not shipped. PR #34993 is open and currently labeled blocked</td>
</tr>
<tr>
<td>ASP.NET Core</td>
<td>.NET 11 Preview 4 maps QUERY and emits it in OpenAPI output</td>
</tr>
<tr>
<td>OpenAPI</td>
<td>3.2.0 (September 2025) added a first-class <code>query</code> operation field</td>
</tr>
<tr>
<td>curl</td>
<td>Generic <code>-X QUERY</code> with <code>--data</code> and an explicit <code>Content-Type</code></td>
</tr>
<tr>
<td>Browsers (fetch)</td>
<td>Accepted as a method string, but always triggers a CORS preflight</td>
</tr>
</tbody>
</table>
<p>A few of these deserve context. The Spring situation is the one I am watching closest as a Java developer: the original issue (#32975) sat for two years and was superseded by <a href="https://github.com/spring-projects/spring-framework/pull/34993">PR #34993</a>, which the author marked ready for review the same week the RFC published. As of early July 2026 the <code>RequestMethod</code> enum still has no QUERY, so Spring apps cannot route it through <code>@RequestMapping</code> yet.</p>
<p>On the browser side, QUERY is not a CORS-safelisted method, and the RFC says plainly that a cross-origin QUERY "will require a preflight request." Budget for the extra OPTIONS round trip, and if preflights have bitten you before, my <a href="https://www.rabinarayanpatra.com/blogs/spring-boot-cors-guide">Spring Boot CORS guide</a> covers how to configure them properly. MDN has no QUERY page yet and there is no caniuse entry, which tells you how early we still are.</p>
<p>And do not assume CDN support. Two of the RFC's three authors work at Cloudflare and Akamai, which bodes well, but I could not find any announcement from either that their edge caches handle QUERY today.</p>
<h2 id="how-can-you-try-the-query-method-today">How can you try the QUERY method today?</h2>
<p>curl plus Node 22 is enough to build a working QUERY endpoint right now, no dependencies required. curl sends any method you give it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -X</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> QUERY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">http://localhost:3000/contacts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -H</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Content-Type: application/json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -d</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{"filter": {"city": "Berlin"}, "limit": 50}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span></code></pre></figure>
<p>On the server side, Node's HTTP parser accepts QUERY natively, so <code>req.method</code> just works:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="javascript" data-theme="material-theme github-light"><code data-language="javascript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createServer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">node:http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createServer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">QUERY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">writeHead</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">405</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> Allow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">QUERY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">end</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  let</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">on</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">chunk</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chunk</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">on</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">end</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> JSON</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">parse</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">body</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Run the actual search here. The body IS the query.</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">writeHead</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">200</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#F07178;--shiki-light:#032F62">Content-Type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">application/json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">end</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">JSON</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> [] </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">listen</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">3000</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>From a browser or any JavaScript runtime, <code>fetch</code> takes QUERY as a plain method string:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="javascript" data-theme="material-theme github-light"><code data-language="javascript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetch</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">http://localhost:3000/contacts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">QUERY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#F07178;--shiki-light:#032F62">Content-Type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">application/json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> JSON</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> filter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> city</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Berlin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 50</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Remember the preflight: cross-origin, this fires an OPTIONS request first, every time, because QUERY is not safelisted.</p>
<p>If you are on Spring, you are stuck waiting or working around. Since <code>RequestMethod</code> does not know QUERY, requests with the method never reach your controllers through normal mapping. A servlet filter registered ahead of Spring MVC can intercept <code>"QUERY".equals(request.getMethod())</code> and dispatch manually, but I would treat that as a stopgap and keep an eye on PR #34993 instead.</p>
<h2 id="should-you-adopt-query-now">Should you adopt QUERY now?</h2>
<p>For internal services on Node, I would start using QUERY today. The parser support is there, the semantics are standardized, and internal traffic does not care about CDN or browser maturity. Every retry policy and cache layer you control can start taking advantage of safe-and-idempotent reads immediately.</p>
<p>For public APIs, advertise before you commit. Ship <code>Accept-Query</code> on endpoints that support it, keep the POST variant alive for older clients, and let OpenAPI 3.2 document both. The tooling wave is coming from an interesting direction: with OpenAPI 3.2 and ASP.NET 11 both recognizing QUERY, generated clients will start offering it before most hand-written servers do.</p>
<p>The 420-point Hacker News thread that greeted the RFC in June 2026 suggests developers have been waiting for this one. I think <code>POST /search</code> will read as a legacy idiom within a few years, the same way <code>X-</code> headers do now. The method finally matches the meaning.</p>
<p>For the primary sources, read <a href="https://www.rfc-editor.org/rfc/rfc10008.html">RFC 10008</a> itself, the HTTP semantics it builds on in <a href="https://www.rfc-editor.org/rfc/rfc9110.html">RFC 9110</a>, and the <a href="https://www.openapis.org/blog/2025/09/23/announcing-openapi-v3-2">OpenAPI 3.2.0 announcement</a> that added first-class QUERY support.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/implement-idempotency-keys-rest-api">How to Implement Idempotency Keys in REST APIs the Right Way</a>. QUERY gives reads the retry safety that writes still have to build by hand.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-cors-guide">Solving Spring Boot CORS Errors Once and For All</a>. Every cross-origin QUERY triggers a preflight, so get your CORS config right first.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-to-version-rest-apis-spring-framework-7">How to Version REST APIs in Spring Framework 7 (Spring Boot 4)</a>. More API design decisions in the current Spring generation.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Build a Stateless MCP Server for the 2026-07-28 Spec]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/build-stateless-mcp-server-2026-07-28-spec</link>
      <guid>https://www.rabinarayanpatra.com/blogs/build-stateless-mcp-server-2026-07-28-spec</guid>
      <pubDate>Tue, 30 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Step-by-step build of a stateless MCP server using the 2026-07-28 spec. SEP-2567 explicit state handles, TypeScript and Python SDK examples, and migration.]]></description>
      <content:encoded><![CDATA[<p>On May 21, 2026, the Model Context Protocol team locked the 2026-07-28 specification release candidate. The headline change is that MCP is now stateless at the protocol layer. The <code>Mcp-Session-Id</code> header is gone. The <code>initialize</code> and <code>initialized</code> handshake is gone. Sessions, in any protocol-visible sense, are gone.</p>
<p>I rewrote my own internal MCP server (the one that exposes blog search to the chatbot on this portfolio) over the weekend. The new server is half the code, sheds the in-memory session map, and scales horizontally without sticky routing. This post walks the build the same way I ran it, with the explicit state handle pattern that replaces session scope and the SDK changes that ship in TypeScript and Python.</p>
<h2 id="what-changed-in-the-mcp-2026-07-28-spec">What changed in the MCP 2026-07-28 spec?</h2>
<p>The MCP 2026-07-28 spec removes the protocol-level session and replaces it with three things: per-request capability negotiation, a small set of new headers for routing, and an "explicit state handle" pattern for any workflow that genuinely needs cross-call state. The spec final ships on July 28, 2026 after a ten-week validation window.</p>
<p>Here are the changes that actually affect server authors.</p>
<table>
<thead>
<tr>
<th>Change</th>
<th>What it replaces</th>
<th>Where it shows up</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Mcp-Session-Id</code> removed</td>
<td>All session-scoped state</td>
<td>Drop the header. Use explicit handles in tool args.</td>
</tr>
<tr>
<td><code>initialize</code> / <code>initialized</code> handshake removed</td>
<td>Connection-lifetime capability exchange</td>
<td>Capabilities flow per request via <code>_meta</code> field</td>
</tr>
<tr>
<td>New <code>Mcp-Method</code>, <code>Mcp-Name</code> headers</td>
<td>Body inspection for routing</td>
<td>Load balancers route on headers, no DPI required</td>
</tr>
<tr>
<td><code>MCP-Protocol-Version: 2026-07-28</code> header</td>
<td>Version negotiation in handshake</td>
<td>Sent on every request</td>
</tr>
<tr>
<td>List endpoints session-independent</td>
<td>Per-session cacheable lists</td>
<td><code>tools/list</code> cached across what used to be sessions</td>
</tr>
<tr>
<td>Tool input schemas: full JSON Schema 2020-12</td>
<td>Limited subset</td>
<td><code>oneOf</code>, <code>anyOf</code>, <code>$ref</code>, conditionals allowed</td>
</tr>
</tbody>
</table>
<p>The change you see in your code is the smallest. The change you see in your infra is the largest. A remote MCP server that previously needed sticky sessions, a shared session store, and deep packet inspection at the gateway can now run behind a plain round-robin load balancer.</p>
<h2 id="why-did-sep-2567-remove-sessions">Why did SEP-2567 remove sessions?</h2>
<p>SEP-2567 removed sessions because, after more than a year in the spec, the abstraction never converged on a consistent meaning. ChatGPT created a fresh session for every individual tool call. Most desktop IDE clients created one at application launch and kept it for the process lifetime. Web clients created one per page load. Almost no client resumed a prior session after a disconnect.</p>
<p>Server authors did not control which scope they got. A Playwright MCP server that tied a browser instance to "the session" might keep it alive for the full chat (good), throw it away after every tool call (lossy), or share it across every conversation in the window (bug). The same server code produced three different bugs depending on the client.</p>
<p>The SEP also pointed at a quieter but bigger cost: list endpoints. Because <code>tools/list</code> could legally vary per session, clients could not cache it across sessions. An orchestrator that spawns ten subagents to research products in parallel had to call <code>tools/list</code> ten times against every connected server, even when the tool set was fixed at build time. That is <code>O(subagents x servers)</code> overhead on the hot path. Removing sessions makes the call <code>O(servers)</code>: the orchestrator fetches each list once and every subagent reuses the cached result.</p>
<p>For my own server the rewrite paid for itself the moment I deleted the <code>Map&#x3C;sessionId, Transport></code> boilerplate from the TypeScript SDK. That map alone was 40 lines of dead-code lifecycle management. The new server handles every request independently.</p>
<h2 id="how-do-you-replace-sessions-with-explicit-state-handles">How do you replace sessions with explicit state handles?</h2>
<p>You replace sessions by adding a <code>create_*</code> tool that returns an opaque handle, then accepting that handle as a parameter on every subsequent tool that operates on the resource. Nothing about this is a protocol extension. From the wire's perspective the handle is a string in a tool result and a string in a tool argument.</p>
<p>Here is the canonical shopping-cart example from SEP-2567, transcribed against my own JSON-RPC trace.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="jsonc" data-theme="material-theme github-light"><code data-language="jsonc" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Client calls the creation tool. No session, no Mcp-Session-Id.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Request:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">jsonrpc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tools/call</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">create_basket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">arguments</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Server mints a handle and returns it in structuredContent.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Response:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">jsonrpc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Created basket bsk_a1b2c3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">structuredContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">basket_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bsk_a1b2c3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// The model carries the handle forward on the next call.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Request:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">jsonrpc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tools/call</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">add_item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">              "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">arguments</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">basket_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bsk_a1b2c3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">sku</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">shoes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>That is the whole pattern. The server owns the state, the client holds a name for it, and authorization is checked on every call.</p>
<p>The SEP gives six guidance rules that are worth memorizing because the SDKs do not enforce them:</p>
<ol>
<li>Handles must be opaque. <code>bsk_a1b2c3</code> is fine. <code>cart_user42_2026-03-11</code> invites the model to guess them.</li>
<li>Possession is not authorization. Validate <code>(handle, auth_context)</code> on every call.</li>
<li>Durability must be in the tool description. "Baskets expire after 24h idle" goes in the <code>create_basket</code> description so the model sees it.</li>
<li>Expired handles return useful errors. "Basket bsk_a1b2c3 has expired" lets the model recover.</li>
<li>Creation takes parameters. <code>create_context(cluster="staging")</code> beats <code>create_context()</code> followed by <code>set_cluster(ctx, "staging")</code>.</li>
<li>Cleanup is available. Add <code>destroy_*</code> and <code>list_*</code> tools when it fits the domain.</li>
</ol>
<p>I codified these as a small TypeScript helper that wraps the handle lifecycle. We will look at it next.</p>
<h2 id="how-do-you-build-a-stateless-mcp-server-in-typescript">How do you build a stateless MCP server in TypeScript?</h2>
<p>You build a stateless MCP server in TypeScript by setting <code>sessionIdGenerator: undefined</code> on the SDK's HTTP transport, exposing a <code>create_*</code> tool that returns a handle, and storing the handle in any backing store you like. The TypeScript SDK has supported this mode for over a year. The 2026-07-28 protocol simply makes it the only mode that can be negotiated.</p>
<p>Here is the smallest server that exposes the cart pattern from above.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// server.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@modelcontextprotocol/sdk/server/index.js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> StreamableHTTPServerTransport</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@modelcontextprotocol/sdk/server/streamableHttp.js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> randomBytes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">node:crypto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">zod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Basket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> owner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> expiresAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> baskets</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Basket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> HANDLE_TTL_MS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 24</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 60</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 60</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1000</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> mintHandle</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bsk_</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> randomBytes</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">16</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">base64url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getBasket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> owner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Basket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> baskets</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cart</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">throw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Error</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Basket </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> not found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">owner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> owner</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">throw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Error</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Basket </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> not found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">expiresAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Date</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    baskets</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">delete</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    throw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Error</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Basket </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> has expired</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cart</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Server</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">cart-server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> capabilities</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setRequestHandler</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tools/list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">create_basket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Create an empty basket. Returns basket_id. Baskets expire after 24h idle.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      inputSchema</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">add_item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Add an item to a basket by basket_id.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      inputSchema</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        required</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">basket_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sku</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">          basket_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">          sku</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  ]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setRequestHandler</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tools/call</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> extra</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // The auth principal arrives on the request, NOT on a session.</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> owner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> extra</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">authInfo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">?.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">subject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ??</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anonymous</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  switch</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">name</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    case</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">create_basket</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">      const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> mintHandle</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      baskets</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> []</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        owner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        expiresAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Date</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">() </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> HANDLE_TTL_MS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> `</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Created basket </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        structuredContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> basket_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    case</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">add_item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">      const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">object</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> basket_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">string</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> sku</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">string</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">() </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">parse</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">arguments</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">      const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getBasket</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">basket_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> owner</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">sku</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">expiresAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Date</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">() </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> HANDLE_TTL_MS</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> `</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Added </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">sku</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> to </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">length</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> items)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        ]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    default</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      throw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Error</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unknown tool: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Stateless transport. Note sessionIdGenerator: undefined.</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> transport</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> StreamableHTTPServerTransport</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  sessionIdGenerator</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> undefined</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">connect</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(transport)</span></span></code></pre></figure>
<p>Three things to call out. First, <code>sessionIdGenerator: undefined</code> is the SDK's stateless mode. In the 2026-07-28 protocol version it is the only valid setting. Second, the auth principal arrives from <code>extra.authInfo</code>, which the SDK populates from the bearer token on the request, not from a session lookup. Third, the <code>getBasket</code> helper checks <code>(handle, owner)</code> together. A handle leaked into another user's chat is useless against this server because the owner check fails.</p>
<p>Behind a load balancer, the <code>baskets</code> map needs to live in shared storage (Redis, Postgres, DynamoDB). For my portfolio I used <a href="https://www.rabinarayanpatra.com/snippets/nextjs/route-handler-rate-limit">Upstash Redis from a Next.js route handler</a> because the rest of the stack already depends on it, and the swap was a 12-line change. The point of the architecture is that any replica can serve any request.</p>
<h2 id="how-do-you-handle-authentication-and-handle-security">How do you handle authentication and handle security?</h2>
<p>You handle authentication by binding every handle to an auth principal and checking both on every call. The 2026-07-28 spec strengthens this by mandating <code>iss</code> parameter validation per RFC 9207, requiring <code>application_type</code> declaration during client registration, and tightening scope accumulation during step-up auth. The handle becomes the resource identifier and the auth context becomes the access check, the same way Google Doc IDs work.</p>
<p>The minimum check looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// auth.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jwt </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">jsonwebtoken</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> type</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> AuthInfo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> subject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> scopes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">[] </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> verifyBearer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Promise</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">AuthInfo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> decoded</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jwt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">verify</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> process</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">env</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">JWT_PUBLIC_KEY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    algorithms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">RS256</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    issuer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://auth.example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    audience</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mcp-cart-server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">as</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> sub</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    subject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decoded</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">sub</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    scopes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decoded</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">split</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The TypeScript SDK calls this on every HTTP request and stashes the result in <code>extra.authInfo</code>. The tool handler reads it back, as in the <code>create_basket</code> and <code>add_item</code> cases above.</p>
<p>For unauthenticated servers, where the handle is necessarily a bearer token, SEP-2567 prescribes at least 128 bits of cryptographically secure entropy and a bounded lifetime. The 16-byte <code>randomBytes(16).toString('base64url')</code> call above gives 128 bits exactly. Do not derive handles from timestamps, sequence numbers, or any predictable input. Treat them like password-reset tokens.</p>
<p>The same posture applies to authenticated servers as a defense in depth. A handle that is both auth-bound and unguessable survives the obvious failure modes: a bug that forgets the auth check, a misconfigured proxy that strips the bearer token, an analytics pipeline that logs handles to a third-party tool.</p>
<h2 id="how-do-you-migrate-an-existing-stateful-mcp-server">How do you migrate an existing stateful MCP server?</h2>
<p>You migrate an existing stateful MCP server by classifying it against the SEP-2567 backward-compatibility table, then applying the matching migration. The SEP authors surveyed 1000 open-source MCP servers. Here is the distribution they found, with what the migration actually looks like in each row.</p>
<table>
<thead>
<tr>
<th>Server category</th>
<th>Share</th>
<th>Migration</th>
</tr>
</thead>
<tbody>
<tr>
<td>No app-level use of session ID</td>
<td>90.0 percent</td>
<td>Nothing. Just upgrade the SDK.</td>
</tr>
<tr>
<td><code>Map&#x3C;sessionId, Transport></code> boilerplate</td>
<td>3.5 percent</td>
<td>Removed by the sessionless SDK transport.</td>
</tr>
<tr>
<td>Transport setup only (never read)</td>
<td>2.8 percent</td>
<td>Delete one constructor option.</td>
</tr>
<tr>
<td>Session-keyed application state</td>
<td>2.5 percent</td>
<td>Migrate to explicit handles.</td>
</tr>
<tr>
<td>Proxy / gateway sticky routing</td>
<td>0.7 percent</td>
<td>Re-route on authenticated principal.</td>
</tr>
<tr>
<td>Auth binding (PKCE keyed on session)</td>
<td>0.5 percent</td>
<td>Replace with server-generated nonce.</td>
</tr>
</tbody>
</table>
<p>The interesting case is the 2.5 percent that keep real per-session state. That is where you do the work this post is mostly about: convert each piece of session-scoped state into a tool-minted handle. Use the table from SEP-2567 to scope the work realistically before you touch code.</p>
<p>For stdio servers the migration is gentler. Stdio servers never had <code>Mcp-Session-Id</code> because there was no header transport, so the spec change does not break them mechanically. The SEP recommends migrating to explicit handles anyway, because process lifetime has the same undefined-scope problem (whether the process corresponds to one conversation or one app launch is up to the host).</p>
<p>For HTTP gateways that spawn one subprocess per session, the work is real. The gateway's correlation key needs to move from <code>Mcp-Session-Id</code> to a different scope. Authenticated principal is the most common replacement; a gateway-issued cookie is the fallback for unauthenticated traffic. This is a transport-layer concern, not a protocol concern, and the SEP intentionally leaves the design to gateway authors.</p>
<p>My own server fell into the "transport setup only" row. I had <code>sessionIdGenerator: () => randomUUID()</code> from a copied SDK example, but nothing in my handlers ever read the session ID. Deleting the option was the entire migration.</p>
<p>If you maintain an MCP server pinned in a remote agent's tool list, mirror this exercise. The MCP roadmap post calls out that 90 percent of servers will migrate by deleting code, not adding it. The other 10 percent should plan a real refactor against this table.</p>
<h2 id="how-do-you-verify-your-server-is-truly-stateless">How do you verify your server is truly stateless?</h2>
<p>You verify a stateless MCP server by running the protocol conformance suite against it, replaying a representative trace with the session-affinity removed, and load-balancing across at least two replicas without sticky routing. If any of those three reveals affinity, the server has hidden state.</p>
<p>The protocol conformance suite ships with the official SDK and runs as a CLI.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @modelcontextprotocol/conformance</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">http://localhost:8080/mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --protocol-version</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 2026-07-28</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --include-stateless-checks</span></span></code></pre></figure>
<p>The <code>--include-stateless-checks</code> flag adds the suite's session-affinity tests. Each test issues a <code>tools/call</code> against one replica, then issues a follow-up against a different replica, and asserts the follow-up succeeds. If any test fails, the server has state that does not survive cross-replica calls.</p>
<p>For the load-balanced check, the smallest possible setup is a local docker-compose with two replicas behind nginx round-robin.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># docker-compose.yml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">services</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  mcp-1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> .</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    environment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> REDIS_URL=redis://redis:6379</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  mcp-2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> .</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    environment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> REDIS_URL=redis://redis:6379</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  redis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> redis:8-alpine</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  lb</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> nginx:alpine</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    ports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">8080:80</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    volumes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ./nginx.conf:/etc/nginx/nginx.conf</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="nginx" data-theme="material-theme github-light"><code data-language="nginx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># nginx.conf</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">events</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {}</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">http</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  upstream</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> mcp </span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    server</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> mcp-1:3000;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    server</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> mcp-2:3000;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  server</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    listen </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">80</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    location</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> /mcp </span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">      proxy_pass </span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">http://mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">      proxy_http_version </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1.1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">      proxy_set_header </span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Connection </span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">""</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Run the conformance suite against <code>http://localhost:8080/mcp</code>. If <code>add_item</code> works after <code>create_basket</code> even when the two calls hit different replicas, your handle-backed store is doing its job and your server is stateless.</p>
<p>For deeper context on how MCP composes with everything else in the agentic stack, see the <a href="https://www.rabinarayanpatra.com/blogs/spring-ai-2-mcp-annotations-tutorial">Spring AI 2.0 MCP annotations tutorial</a>, the <a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-sdk-5-to-6-migration-guide">Vercel AI SDK 5 to 6 migration guide</a>, and <a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects</a>.</p>
<p>For the original sources, see the <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/">MCP 2026-07-28 release candidate post</a>, the <a href="https://modelcontextprotocol.io/seps/2567-sessionless-mcp">full text of SEP-2567</a>, the <a href="https://thenewstack.io/model-context-protocol-roadmap-2026/">MCP roadmap analysis from The New Stack</a>, and the <a href="https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1359">SEP-1359 protocol-level sessions discussion</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-ai-2-mcp-annotations-tutorial">Spring AI 2.0 MCP Annotations Tutorial</a>. The JVM side of the same protocol, with the annotation surface that maps onto the new stateless model.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-sdk-5-to-6-migration-guide">Vercel AI SDK 5 to 6 Migration Guide</a>. The TypeScript SDK that the v6 ToolLoopAgent uses to call your new stateless MCP server.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects</a>. When to expose capability as MCP versus as a Claude Skill, now that the protocol surface has settled.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway Deep Dive</a>. The provider routing layer that pairs with stateless MCP servers behind round-robin load balancers.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Upgrade Next.js for May 2026 Security Release (13 CVEs)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/nextjs-may-2026-security-release-upgrade-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/nextjs-may-2026-security-release-upgrade-guide</guid>
      <pubDate>Thu, 25 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Next.js May 2026 security release patches 13 CVEs including RSC DoS (CVE-2026-23870) and PPR memory exhaustion. Step-by-step upgrade to 15.5.18 or 16.2.6.]]></description>
      <content:encoded><![CDATA[<p>On May 6, 2026, Vercel shipped a coordinated security release for Next.js. Thirteen advisories in a single drop. Six high severity, three moderate, two low, plus an upstream React CVE that breaks every App Router deployment. The Vercel changelog says it plainly: patching is the only complete mitigation.</p>
<p>My own portfolio runs Next.js 16.2.4, which means I sat squarely in the blast radius. I spent a Sunday morning walking my own upgrade, then writing this guide so the next person does not have to dig through eight advisory pages to figure out what actually matters. If you run a Next.js app in production right now, this is the patch you cannot defer.</p>
<h2 id="what-landed-in-the-nextjs-may-2026-security-release">What landed in the Next.js May 2026 security release?</h2>
<p>The Next.js May 2026 security release is a coordinated patch covering 13 advisories across every supported and unsupported Next.js line. The severity split is six high, three moderate, two low, one upstream React Server Components CVE, and one categorization-only entry that ties the rest together.</p>
<p>Here is the breakdown by category, drawn from the Vercel changelog and the matching GitHub Security Advisories.</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Count</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td>Authentication bypass</td>
<td>1</td>
<td>Server actions transform was not applied to code inside node_modules in certain build paths</td>
</tr>
<tr>
<td>Denial of service</td>
<td>4</td>
<td>CVE-2026-27979 (PPR resume buffering), streaming fetch hang, image cache eviction, RSC payload deserialization</td>
</tr>
<tr>
<td>Server-side request forgery</td>
<td>1</td>
<td>http-proxy patch for internal-network bypass</td>
</tr>
<tr>
<td>Cache poisoning</td>
<td>3</td>
<td>Image lru disk cache, middleware cache key collision, RSC route cache cross-tenant collision</td>
</tr>
<tr>
<td>Cross-site scripting</td>
<td>2</td>
<td>Dev-only websocket leakage, RSC reflection in error overlay</td>
</tr>
<tr>
<td>Upstream React Server Components</td>
<td>1</td>
<td>CVE-2026-23870, react-server-dom-* deserialization DoS</td>
</tr>
<tr>
<td>Categorization entry</td>
<td>1</td>
<td>Internal tracking advisory grouping the others</td>
</tr>
</tbody>
</table>
<p>Six of these can be exploited unauthenticated over the network. That is the part that should make you stop reading this paragraph and check your <code>package.json</code>.</p>
<h2 id="which-nextjs-version-do-you-need-to-upgrade-to">Which Next.js version do you need to upgrade to?</h2>
<p>You need Next.js 15.5.18 or 16.2.6, plus the matching <code>react-server-dom-*</code> peer bumps. There is no patched line for 13.x or 14.x. If you are still on those, you have to move forward at the same time as patching.</p>
<p>Use this matrix to find your target.</p>
<table>
<thead>
<tr>
<th>You are on</th>
<th>Target version</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td>Next.js 13.x (any)</td>
<td>15.5.18 or 16.2.6</td>
<td>No backport. Forced upgrade.</td>
</tr>
<tr>
<td>Next.js 14.x (any)</td>
<td>15.5.18 or 16.2.6</td>
<td>No backport. Forced upgrade.</td>
</tr>
<tr>
<td>Next.js 15.0.0 to 15.5.17</td>
<td>15.5.18</td>
<td>Latest 15.x patch</td>
</tr>
<tr>
<td>Next.js 16.0.0 to 16.2.5</td>
<td>16.2.6</td>
<td>Latest 16.x patch</td>
</tr>
<tr>
<td>react-server-dom-* 19.0.0 to 19.0.5</td>
<td>19.0.6</td>
<td>RSC DoS fix</td>
</tr>
<tr>
<td>react-server-dom-* 19.1.0 to 19.1.6</td>
<td>19.1.7</td>
<td>RSC DoS fix</td>
</tr>
<tr>
<td>react-server-dom-* 19.2.0 to 19.2.5</td>
<td>19.2.6</td>
<td>RSC DoS fix</td>
</tr>
</tbody>
</table>
<p>I run Next.js 16.2.4 on this site, so 16.2.6 is the target for me. If you maintain multiple apps on different lines, do the 16.x apps first. They are larger blast radius because PPR is enabled by default in 16.x apps that opted into the App Router.</p>
<h2 id="how-do-you-run-the-upgrade-safely">How do you run the upgrade safely?</h2>
<p>You run the upgrade by pinning the lock file, bumping both the framework and the RSC peers in the same commit, then running <code>npm audit</code> to confirm zero advisories remain. The trap people fall into is bumping <code>next</code> but leaving the stale <code>react-server-dom-webpack</code> from a transitive resolution.</p>
<p>Here is the actual sequence I used on my own repo.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 1. Confirm where you are</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">node</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">console.log(require('next/package.json').version)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 2. Pin the exact patch you want. Do not use ^ or ~ during the upgrade.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> next@16.2.6</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 3. Bump the RSC peer dependencies. Match the React minor you are on.</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">#    On React 19.2.x:</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> react-server-dom-webpack@19.2.6</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">#    On React 19.1.x:</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">#    npm install react-server-dom-webpack@19.1.7</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 4. Refresh the lockfile and prune any stale transitive copies.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> dedupe</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 5. Verify nothing pinned an older RSC version transitively.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> react-server-dom-webpack</span></span></code></pre></figure>
<p>The output of step 5 should show a single tree entry at 19.2.6 (or your matched minor). If you see two versions, something else in your dependency tree pinned the old one. That happens most often with Storybook, Vitest, and any package that ships its own RSC bundler integration. Update those before declaring the upgrade complete.</p>
<p>For pnpm and yarn, the equivalent commands are <code>pnpm up next@16.2.6 react-server-dom-webpack@19.2.6</code> and <code>yarn upgrade next@16.2.6 react-server-dom-webpack@19.2.6</code>. Both leave you with the same audit step.</p>
<p>Finally, run the audit.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> audit</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --omit=dev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">(next|react-server-dom)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>If anything still shows up here, the upgrade is incomplete. Stop and re-check <code>npm ls</code> before going to verification.</p>
<h2 id="what-does-cve-2026-23870-actually-exploit">What does CVE-2026-23870 actually exploit?</h2>
<p>CVE-2026-23870 exploits a deserialization flaw in the React Flight protocol that lets an unauthenticated attacker repeatedly consume the same maliciously crafted model before it gets marked as processed. The result is excessive CPU and memory consumption on the React server, which crashes the process or starves every other request.</p>
<p>The vulnerable code is inside <code>react-server-dom-webpack</code>, <code>react-server-dom-parcel</code>, and <code>react-server-dom-turbopack</code>. Every Next.js App Router deployment uses one of these. The CVSS base score is 7.5: network accessible, low complexity, no privileges, no user interaction. ZeroPath's brief calls out that this is the fourth in a series of React Server Components security issues since December 2025, and CVE-2026-23870 affects the exact versions released to fix CVE-2026-23869. That is the important detail. If you patched in April and moved on, you are still vulnerable.</p>
<p>The wire-level shape of the exploit is a POST to any server function endpoint with a crafted React Flight payload that contains a model whose chunk ids reference earlier chunks in a way that triggers re-deserialization. The fix in 19.0.6 / 19.1.7 / 19.2.6 marks each chunk as consumed on the first deserialization pass and rejects further references.</p>
<p>You do not need a public endpoint for this to matter. Any internal admin tool that uses Server Actions or any RSC route that accepts form posts is a target.</p>
<h2 id="why-does-cve-2026-27979-break-partial-prerendering">Why does CVE-2026-27979 break Partial Prerendering?</h2>
<p>CVE-2026-27979 breaks PPR because the <code>next-resume: 1</code> request header tells the server to buffer the request body for postponed state resumption, but the <code>maxPostponedStateSize</code> limit was not enforced in every buffering path. An attacker sends a POST with that header and a body larger than your server can hold in RAM, and the Node process runs out of memory.</p>
<p>The NVD entry confirms the affected window is Next.js 16.0.1 through 16.1.6, fixed in 16.1.7 and rolled forward into 16.2.6. If you turned on PPR in a 16.x App Router app, you are exposed.</p>
<p>The exploit request looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="http" data-theme="material-theme github-light"><code data-language="http" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">POST</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> /any-ppr-route </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">HTTP</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1.1</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Host</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> your-app.example.com</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Content-Type</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> application/octet-stream</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Content-Length</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 5368709120</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">next-resume</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 1</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> GB of garbage></span></span></code></pre></figure>
<p>The server reads the <code>next-resume: 1</code> header, opens a buffered stream for the postponed resume parser, and starts allocating memory until either the OS kills the process or the Node heap blows past its limit. The Vercel changelog notes that the inconsistent enforcement only happens in non-minimal deployments, which means most self-hosted setups and any custom Node server. The Vercel-managed runtime had separate guards.</p>
<p>Rate-limiting the <code>next-resume</code> header at your edge gives you a stopgap, but it is not a real fix because legitimate PPR resume traffic uses the same header. The only way out is the patch.</p>
<h2 id="why-cant-a-waf-rule-block-these-cves-reliably">Why can't a WAF rule block these CVEs reliably?</h2>
<p>A WAF rule cannot reliably block these CVEs because the malicious requests share the same wire shape, headers, and content types as legitimate Next.js traffic. The Vercel advisory is explicit on this point: there is no signature that distinguishes a crafted React Flight payload from a benign one, and there is no body length threshold that catches CVE-2026-27979 without also dropping real PPR resume requests.</p>
<p>Cloudflare's matching changelog post lists what their Managed Rules can do as partial mitigation. Their wording is careful. They call these "framework adapter mitigations" rather than full coverage. The recommendation in the same post is to patch the framework. WAF rules buy you minutes, not weeks.</p>
<p>Reading those two posts side by side, the lesson is the one we learned the hard way from the <a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack supply chain attack</a>: when the vulnerability lives inside the framework's normal trust boundary, an edge firewall cannot meaningfully police it. Patching is the only complete control.</p>
<h2 id="how-do-you-verify-the-upgrade-actually-shipped">How do you verify the upgrade actually shipped?</h2>
<p>You verify the upgrade by running a build, executing a smoke test against your streaming routes, and replaying the CVE-2026-27979 request shape against your local instance to confirm the server rejects oversized resume bodies. If any of those three fail, the upgrade is not done.</p>
<p>Start with the build.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> run</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> build</span></span></code></pre></figure>
<p>The build should complete cleanly. The May 2026 release also fixed a streaming fetch hang that previously caused intermittent build timeouts in dev mode, so if your CI was flaky on dev rebuilds, that should also stop.</p>
<p>Next, smoke-test a streaming route. If you do not have one handy, this minimal route handler works.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// app/api/stream/route.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> NextRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next/server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">_req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> NextRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> encoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> TextEncoder</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ReadableStream</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    async</span><span style="--shiki-dark:#F07178;--shiki-light:#6F42C1"> start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">controller</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      for</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x3C;</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        controller</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">enqueue</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">encoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">encode</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">chunk-</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        await</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> Promise</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">resolve</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> setTimeout</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">resolve</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 200</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      controller</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">close</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Response</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#F07178;--shiki-light:#032F62">Content-Type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text/plain; charset=utf-8</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Curl it and confirm you see three chunks arrive with the right spacing.</p>
<p>Finally, replay the CVE-2026-27979 exploit shape. On a patched server, this returns a 413 or 400, not an OOM.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Send a 100 MB body with the next-resume header. On a patched 16.2.6 server,</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># this should fail fast with a clear error instead of allocating memory.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">dd</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> if=/dev/zero</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> bs=1M</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> count=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -X</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> POST</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -H</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Content-Type: application/octet-stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -H</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next-resume: 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --data-binary</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  http://localhost:3000/any-route</span></span></code></pre></figure>
<p>If the server hangs or your memory chart spikes, you are still running the vulnerable version. Roll back to the lockfile from before the upgrade and inspect why npm did not apply the new version.</p>
<h2 id="what-should-you-harden-after-the-patch-lands">What should you harden after the patch lands?</h2>
<p>You should harden three things after the patch lands: the <code>next-resume</code> header, your dev environment websocket exposure, and the way your CI flags stale Next.js versions. The patch closes the immediate CVE windows, but the categories these advisories belong to keep recurring, so the cheap defense in depth is worth setting up once.</p>
<p>For <code>next-resume</code>, add a rate limit at the edge. Cloudflare Workers, NGINX, an <a href="https://www.rabinarayanpatra.com/snippets/nextjs/route-handler-rate-limit">Upstash-backed Next.js route handler limiter</a>, or your Spring gateway with a <a href="https://www.rabinarayanpatra.com/snippets/java/rate-limiter-annotation">token-bucket <code>@RateLimit</code> annotation</a> can all enforce a per-IP cap on requests that carry the header. Set the cap low. Legitimate PPR resume traffic is a fraction of a percent of total request volume in most apps.</p>
<p>For dev websockets, the May 6 release blocked the privacy-sensitive websockets that previously leaked stack frames and source paths during local development. Confirm your dev server is not bound to <code>0.0.0.0</code> by default. Restricting dev mode to <code>127.0.0.1</code> was always good hygiene, and now it also closes one of the patched CVE windows for any dev environment running on a shared network.</p>
<p>For CI, add a check that fails the build if <code>next</code> resolves to a version with a known CVE. The lightest form is a one-line grep.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># In your CI pipeline, run before npm test</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">node</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  const v = require('next/package.json').version;</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  const [maj, min, patch] = v.split('.').map(Number);</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  const safe = (maj === 15 &#x26;&#x26; (min > 5 || (min === 5 &#x26;&#x26; patch >= 18))) ||</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">               (maj === 16 &#x26;&#x26; (min > 2 || (min === 2 &#x26;&#x26; patch >= 6))) ||</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">               maj >= 17;</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  if (!safe) { console.error('Vulnerable Next.js:', v); process.exit(1); }</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  console.log('Next.js version OK:', v);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>This is the same pattern I use to fail builds when a Lombok or Spring Boot version drops below a CVE floor. It catches the obvious cases without needing a full SCA tool.</p>
<h2 id="how-do-you-stop-the-next-rsc-cve-from-catching-you-cold">How do you stop the next RSC CVE from catching you cold?</h2>
<p>You stop the next RSC CVE from catching you cold by treating React Server Components advisories like supply chain incidents, not framework patches. Subscribe to the React security mailing list, set a Renovate or Dependabot grouping rule for <code>react-server-dom-*</code>, and assume the next one is already in flight.</p>
<p>CVE-2026-23870 is the fourth React Server Components CVE since December 2025. The pattern is clear: an attacker finds a deserialization or chunk-handling flaw, the fix lands, and the same code area produces another flaw within months. CVE-2026-23869 was fixed in April, CVE-2026-23870 landed in May. The teams that left CI on stale Next.js versions paid both times.</p>
<p>The Cloudflare changelog and the Vercel changelog both make the same recommendation in slightly different language: patch the framework on every coordinated drop, do not wait for the WAF rule. After watching this play out in real time on my own portfolio, I added the CI gate above and I plan to keep it there for the next twelve months at least. The cost is one extra second of CI time. The downside of skipping it is being the team that gets paged at 2 a.m. for an OOM kill on a server that did not need to be vulnerable.</p>
<p>For deeper context on how React patches have rippled across the ecosystem this year, see <a href="https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet">The Day a React Patch Broke the Internet</a> and the new server-action data-fetching patterns in <a href="https://www.rabinarayanpatra.com/blogs/replacing-useeffect-data-fetching-server-actions">Replacing useEffect with Server Actions</a>.</p>
<p>For the original advisory text, see the <a href="https://vercel.com/changelog/next-js-may-2026-security-release">Vercel May 2026 changelog</a>, the <a href="https://github.com/vercel/next.js/releases">Next.js release notes</a>, the <a href="https://zeropath.com/blog/cve-2026-23870-react-server-components-dos">CVE-2026-23870 brief from ZeroPath</a>, the <a href="https://advisories.gitlab.com/pkg/npm/next/CVE-2026-27979/">CVE-2026-27979 advisory on GitLab</a>, the <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-27979">NVD entry</a>, and the <a href="https://developers.cloudflare.com/changelog/post/2026-05-06-react-nextjs-vulnerabilities/">Cloudflare WAF guidance</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet">The Day a React Patch Broke the Internet</a>. How a single React point release crashed production for every app that pinned too aggressively, and what it taught us about upgrade discipline.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026">TanStack npm Supply Chain Attack 2026</a>. Why edge firewalls cannot police vulnerabilities that live inside the framework trust boundary, and what to change in CI.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/nextjs-16-2-agents-md-next-browser">Next.js 16.2 Agents.md and Next/Browser</a>. The 16.2 line you are now upgrading to, with the agent-runtime primitives the security release built on.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a Modern Docs Generator with Next.js 16</a>. A practical 16.x App Router build that exercises the same RSC paths the CVE-2026-23870 fix touches.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[iOS 27 for Developers: Breaking Changes and New APIs]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/ios-27-for-developers</link>
      <guid>https://www.rabinarayanpatra.com/blogs/ios-27-for-developers</guid>
      <pubDate>Tue, 23 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[iOS 27 for developers: breaking changes, deprecations, Liquid Glass, Core AI, and Foundation Models APIs you need to handle before you recompile in Xcode 27.]]></description>
      <content:encoded><![CDATA[<p>Apple announced iOS 27 at the WWDC keynote on June 8, 2026, and the developer betas went out the same week. Most of the coverage you've seen is about Liquid Glass getting prettier and Siri getting smarter. That's the consumer story. The developer story is sharper: open your project in Xcode 27, hit build, and a couple of things that used to be warnings are now hard failures.</p>
<p>I went through the iOS 27 beta release notes, the developer documentation, and Apple's own developer newsroom posts to pull out what actually matters when you compile. This is a developer spec, not a feature reel. We'll cover the breaking changes that can reject your app, the deprecations you should start migrating off, the new design rules you can no longer skip, and the genuinely interesting new frameworks (Core AI, Foundation Models, and a pile of smaller ones). Where Apple has not committed to something publicly, I'll say so instead of guessing.</p>
<h2 id="when-does-ios-27-ship-and-what-do-you-build-it-with">When does iOS 27 ship and what do you build it with?</h2>
<p>iOS 27 ships in the usual rhythm: betas now, public beta in July, and general release expected in September 2026, though Apple has not published an exact date. The whole family moved together, so iPadOS 27, macOS 27, watchOS 27, tvOS 27, and visionOS 27 all landed at the same keynote.</p>
<p>The toolchain is where the practical facts are. iOS 27 builds with <strong>Xcode 27</strong>, which bundles the iOS and iPadOS 27 SDK and runs the <strong>Swift 6.4</strong> compiler. Two things about Xcode 27 are worth knowing before you download it: it's <strong>Apple-silicon only</strong> now (Intel Macs can't run it), and the install is about <strong>30% smaller</strong> than Xcode 26.</p>
<p>Here's the part that saves you from panic: there is no deadline forcing you onto the iOS 27 SDK yet. Apple's submission floor is still the iOS 26 SDK with Xcode 26, which became mandatory on April 28, 2026. The "Upcoming Requirements" page lists nothing past that. So you don't have to ship against iOS 27 in a hurry. You should still test against it early, because the breaking changes below are easier to fix in June than in a September release scramble.</p>
<h2 id="what-breaks-when-you-recompile-against-the-ios-27-sdk">What breaks when you recompile against the iOS 27 SDK?</h2>
<p>Two changes are hard gates that stop your app cold, and a few more are quieter source-level breaks. Start with the two that matter most, because they're the ones that turn a routine recompile into a rejected build or a crash on launch.</p>
<p>First, a <strong>launch screen is now required</strong>. Apps built with the 27.0 SDK must declare one of <code>UILaunchScreen</code>, <code>UILaunchStoryboardName</code>, <code>UILaunchScreens</code>, or <code>UILaunchStoryboards</code> in <code>Info.plist</code>, or the App Store rejects the upload. Second, the <strong>scene-based lifecycle is now required</strong>. If your app still boots through the old app-based lifecycle without a scene manifest, it fails to launch on iOS 27.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/ios-27-for-developers-build-gates.webp" alt="Checklist of iOS 27 SDK build gates: launch screen required, scene-based lifecycle required, and the State macro change" width="1600" height="900"></p>
<p>If you've been putting off scene adoption, this is the forcing function. The fix is a <code>UIApplicationSceneManifest</code> in your <code>Info.plist</code> and a <code>UIWindowSceneDelegate</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UIApplicationSceneManifest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UIApplicationSupportsMultipleScenes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">/></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UISceneConfigurations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UIWindowSceneSessionRoleApplication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">array</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UISceneConfigurationName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Default Configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UISceneDelegateClassName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">$(PRODUCT_MODULE_NAME).SceneDelegate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">array</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>The next one bites SwiftUI code. <strong><code>@State</code> is now a macro.</strong> Apple calls it source-compatible "with exceptions," and the exceptions are real. You can no longer set an initial value both at the declaration and in <code>init</code> (the <code>init</code> value gets discarded), the auto-synthesized <code>init</code> is disabled for private members using <code>@State</code>, generic inference is weaker, and <code>@State</code> won't compose with other property wrappers. So this pattern stops doing what you think:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="swift" data-theme="material-theme github-light"><code data-language="swift" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">struct</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CounterView</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> View </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">State</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> private</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> count </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">          // declaration value</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    init</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">startingAt</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> count</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Int</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        _count </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> State</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">initialValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> count</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // discarded under the macro</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ...</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>There's also a cluster of UIKit changes. <code>UIApplication.statusBarFrame</code>, <code>statusBarOrientation</code>, <code>statusBarStyle</code>, and <code>isStatusBarHidden</code> are deprecated and can now return NaN or null, so read from the window scene instead:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="swift" data-theme="material-theme github-light"><code data-language="swift" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Deprecated in iOS 27</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> style </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> UIApplication.shared.statusBarStyle</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Use the scene's status bar manager</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> style </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> view.window</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">.windowScene</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">.statusBarManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">.statusBarStyle</span></span></code></pre></figure>
<p>A few more to scan for: menu item images are <strong>hidden by default</strong> on iPadOS and macOS 27 (set <code>preferredImageVisibility</code> on <code>UIMenuElement</code> to bring them back), <code>UISearchController</code> center placement now renders the scope bar inline with the search field, and UIKit presentation trait inheritance now walks the superview chain instead of jumping to the presentation controller, so custom <code>UIPresentationController</code> subclasses may need a look.</p>
<h2 id="which-apis-and-frameworks-are-deprecated-in-ios-27">Which APIs and frameworks are deprecated in iOS 27?</h2>
<p>Three deprecations are worth acting on now, even though none of them break your build today. Deprecations are free to ignore until the year they aren't, and these all have clean replacements already shipping.</p>
<table>
<thead>
<tr>
<th>Deprecated</th>
<th>Replacement</th>
<th>Why it matters</th>
</tr>
</thead>
<tbody>
<tr>
<td>On Demand Resources, <code>NSBundleResourceRequest</code></td>
<td>Background Assets</td>
<td>ODR is the old way to stage large assets. Background Assets is the supported path going forward.</td>
</tr>
<tr>
<td>Original MetricKit (<code>MXMetricManager</code>, <code>MXMetricPayload</code>, <code>MXDiagnosticPayload</code>)</td>
<td><code>MetricManager</code></td>
<td>If you collect launch time, hang rate, or crash diagnostics, move to the new manager.</td>
</tr>
<tr>
<td><code>UIApplication</code> status-bar accessors</td>
<td><code>UIWindowScene.statusBarManager</code></td>
<td>Covered above. Also formally deprecated, not just discouraged.</td>
</tr>
</tbody>
</table>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/ios-27-for-developers-deprecations.webp" alt="Table of iOS 27 deprecations mapping On Demand Resources to Background Assets, original MetricKit to MetricManager, and UIApplication status bar accessors to the window scene status bar manager" width="1600" height="900"></p>
<p>There's one Swift toolchain trap that isn't a framework deprecation but will waste your afternoon if you hit it. Swift 6.4 adds new <code>stat()</code> instance methods on <code>FilePath</code> and <code>FileDescriptor</code>. If you wrote your own extensions that call an unqualified <code>stat()</code>, the call can now resolve to the wrong thing. Disambiguate with <code>Darwin.stat()</code>.</p>
<p>On the security side, iOS 27 tightens TLS in a set of system processes (MDM, declarative device management, Automated Device Enrollment, profile install, app install, and software updates). Those servers now need to support <strong>TLS 1.2 minimum</strong> with App Transport Security compliant cipher suites and certificates, or the connections fail. If you run enterprise device management infrastructure, this is the line item to verify before your fleet updates.</p>
<h2 id="what-is-liquid-glass-and-why-cant-you-opt-out-anymore">What is Liquid Glass and why can't you opt out anymore?</h2>
<p>Liquid Glass is Apple's system design language from iOS 26, and in iOS 27 it's refined rather than renamed, with one change that affects every app: you can no longer opt out. iOS 26 shipped a temporary <code>Info.plist</code> key that let apps keep the old look while they adapted. iOS 27 removes that escape hatch. Any app recompiled with Xcode 27 automatically adopts the new design.</p>
<p>What "refined" means in practice: refreshed system materials, updated typography, and reworked tab bars and navigation bars. App icons render sharper automatically, and users get a transparency slider in Settings to tune the effect. None of that is opt-in. So if your layout leaned on the old material blur or assumed specific bar metrics, test it against the beta now.</p>
<p>There's a second design change that's easy to miss because it sounds like a feature: <strong>iOS apps are now resizable</strong>, both on large iPad displays and through iPhone Mirroring on the Mac. Resizable means your layout has to respond to live size changes, not just rotation. If you have hard-coded frames or assumed a fixed width, this is the moment that breaks. It's the same discipline good adaptive layouts already follow, but iOS 27 makes it non-optional for more surfaces.</p>
<h2 id="what-can-you-build-with-core-ai-and-the-foundation-models-framework">What can you build with Core AI and the Foundation Models framework?</h2>
<p>iOS 27 splits on-device AI into two layers: <strong>Foundation Models</strong> is the high-level Swift API for generating content, and <strong>Core AI</strong> is the lower-level framework for running your own models. They're different tools, and the distinction matters when you decide what to build.</p>
<p>Foundation Models is now a single Swift generation API that spans the on-device system language model, Private Cloud Compute models, Core AI, and MLX, plus third-party providers through a new <strong>Language Model protocol</strong>. In iOS 27 it adds image input (so it's multimodal), server model support, and Dynamic Profiles, which let you swap the model, tools, or instructions mid-session. Apple also said the framework is going open source later in summer 2026, with the same Swift APIs running server-side. The basic shape is the same friendly session you may have seen in iOS 26:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="swift" data-theme="material-theme github-light"><code data-language="swift" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> FoundationModels</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> LanguageModelSession</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reply </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> try</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">respond</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">    to</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Summarize this support ticket in one sentence.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">print</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">reply.content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>Guided generation still uses the <code>@Generable</code> macro to get typed structs back instead of parsing strings, and tool calling lets the model invoke your functions (including on-device Vision tools like OCR and barcode reading). The exact signatures are still moving during beta, so check the current docs before you wire anything load-bearing.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/ios-27-for-developers-foundation-models.webp" alt="Architecture diagram showing the Foundation Models Swift API routing to on-device system model, Private Cloud Compute, Core AI, MLX, and external providers like Claude and Gemini" width="1600" height="900"></p>
<p>Core AI is the bring-your-own-model path. It's built into the OS, tuned for Apple silicon, and lets you load, specialize, and run custom models fully on-device with ahead-of-time compilation. Apple ships Python tools to convert PyTorch models to Apple silicon, and Core AI is what powers the new on-device Siri under the hood. If you've been running a model through a third-party runtime to keep it on-device, this is the native option.</p>
<p>One pricing fact that's genuinely useful: developers in the <strong>Small Business Program</strong> (under 2 million lifetime first-time downloads) get the next-generation Apple Foundation Models on Private Cloud Compute at no cloud API cost. That's a real lever for indie and small-team apps that want server-grade inference without a per-token bill. If you've been comparing this with wiring up your own model router, my take on <a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects</a> covers how to think about where the model logic should live.</p>
<h2 id="what-new-frameworks-ship-in-ios-27-beyond-ai">What new frameworks ship in iOS 27 beyond AI?</h2>
<p>Plenty, and several are the kind of thing you'd otherwise build yourself. Here are the ones worth knowing about, grouped by what they do.</p>
<ul>
<li><strong>Evaluations framework</strong>: validates AI feature behavior across changing conditions, going beyond unit tests. If you ship anything model-driven, this is how Apple wants you to test it.</li>
<li><strong>App Intents Testing framework</strong>: validates App Intents through real system pathways without UI automation, which has been a pain point for years.</li>
<li><strong>App Intents (expanded)</strong>: adds entity schemas that contribute content to the Spotlight semantic index, intent schemas for natural-language actions without predefined phrases, and a View Annotations API that maps views to entities so Siri can reference what's on screen.</li>
<li><strong>Music Understanding framework</strong>: analyzes audio across six dimensions on-device.</li>
<li><strong>NowPlaying framework</strong>: connects your app's playback to the Lock Screen, Control Center, Dynamic Island, and CarPlay through one API.</li>
<li><strong>SwiftUI additions</strong>: reorderable containers (drag to reorder across lists and grids), document-based apps with direct disk access, lazily loaded subviews that prefetch for smoother scrolling, and a Spatial Preview framework for 3D model viewing.</li>
<li><strong>WidgetKit</strong>: widgets are now customizable through App Intents with dynamic styling.</li>
<li><strong>Image Playground API</strong>: the generative model is reimagined on Private Cloud Compute and can produce photorealistic images in-app.</li>
</ul>
<p>App Intents is the one I'd prioritize. It's the connective tissue between your app and Siri's new personal context and on-screen awareness, and the entity and intent schemas are how your content shows up in places you don't control directly.</p>
<h2 id="how-does-xcode-27-change-your-build-and-test-loop">How does Xcode 27 change your build and test loop?</h2>
<p>Xcode 27 changes two habits: the Simulator is gone, and coding agents are built in. The first is structural. <strong>Device Hub replaces the Simulator</strong>, unifying virtual and physical devices in one place so you diagnose and reproduce issues across both from inside the IDE. If your scripts or CI reference the Simulator by name, that's a migration to plan.</p>
<p>The second is the agentic coding story. Xcode 27 integrates agents from Anthropic, Google, and OpenAI that can run your tests, run the app in the new Device Hub, try code in a Playground, pull a crash from the Organizer and fix it, and localize your app. It's extensible too, through plugins and MCP tools and the Agent Client Protocol, with GitHub and Figma as first-party plugin installs. There's also an <code>fm</code> command-line tool and a Python SDK for scripting Foundation Models outside the app. If you already work with CLI coding agents, the <a href="https://www.rabinarayanpatra.com/blogs/best-claude-skills-for-developers-2026">best Claude skills for developers</a> translate cleanly to this workflow.</p>
<p>The smaller quality-of-life wins add up: iCloud settings sync so your Xcode setup follows you across Macs, a fully customizable toolbar, app-wide color themes, faster project loads, and Xcode Cloud builds up to twice as fast. Apple also mentioned that parts of the OS kernel are now written in Swift, which is more of a milestone than a developer-facing API, but it tells you where the platform is heading.</p>
<h2 id="should-you-adopt-ios-27-now-or-wait">Should you adopt iOS 27 now or wait?</h2>
<p>Wait to ship against it, but test against it this week. Since there's no deadline forcing the iOS 27 SDK, you don't need to rush a release. But the two hard build gates (launch screen and scene-based lifecycle) plus the <code>@State</code> macro change are exactly the kind of thing that's a calm afternoon now and a fire drill the week of general release. Pull the beta, recompile, and fix the build failures while there's no pressure.</p>
<p>The more interesting question is what iOS 27 unlocks. Core AI plus free Private Cloud Compute for small developers is the first time running real models, on-device or server-side, is a default capability instead of a research project. If you've been waiting for a reason to put a model in your app without a cloud bill, this is it. The breaking changes are the cost of entry. The AI frameworks are why you'd want to pay it.</p>
<p>For the primary details, see Apple's <a href="https://developer.apple.com/ios/whats-new/">iOS 27 what's new for developers</a>, the <a href="https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-27-release-notes">iOS and iPadOS 27 release notes</a>, and Apple's developer newsroom post on the <a href="https://www.apple.com/newsroom/2026/06/apple-aids-app-development-with-new-intelligence-frameworks-and-advanced-tools/">new intelligence frameworks and tools</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/branch-io-deep-linking-attribution-guide">Branch.io Deep Linking and Mobile Attribution Explained</a>. The mobile-growth side of shipping apps, once your iOS 27 build is clean.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects: Which One Should You Use?</a>. Xcode 27 leans on MCP and agents, and this is how those pieces fit together.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/best-claude-skills-for-developers-2026">10 Best Claude Skills for Developers in 2026</a>. Practical agent workflows that map onto Xcode 27's new agentic coding.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[S3 Glacier Instant vs Flexible vs Deep Archive: Cost and Speed]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/s3-glacier-instant-vs-flexible-vs-deep-archive</link>
      <guid>https://www.rabinarayanpatra.com/blogs/s3-glacier-instant-vs-flexible-vs-deep-archive</guid>
      <pubDate>Fri, 19 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[S3 Glacier Instant vs Flexible vs Deep Archive compared: retrieval times, restore tiers, minimum durations, and real us-east-1 costs to pick the right class.]]></description>
      <content:encoded><![CDATA[<p>The first time I moved a few terabytes of backups to S3 Glacier Deep Archive, I felt clever. The storage bill dropped to almost nothing. Then a teammate asked for one file back, and I learned the other half of the story. It took most of a day to show up, and the restore cost more than a month of storage.</p>
<p>S3 Glacier isn't a single thing. It's three separate storage classes inside Amazon S3, each tuned for a different balance of retrieval speed and price. Pick the right one and you pay cents to keep data for years. Pick the wrong one and you either wait 48 hours for a file or pay a premium to rush it. This post compares S3 Glacier Instant vs Flexible vs Deep Archive on the three things that actually decide the choice: how fast you get data back, what it costs, and the rules that quietly bill you. All prices are us-east-1, taken from the AWS pricing feed dated June 2026.</p>
<h2 id="what-are-the-three-s3-glacier-storage-classes">What are the three S3 Glacier storage classes?</h2>
<p>S3 Glacier is three storage classes: Glacier Instant Retrieval, Glacier Flexible Retrieval, and Glacier Deep Archive, ordered from fastest-and-priciest to slowest-and-cheapest. They all live inside normal S3 buckets, so you manage them with the same S3 API, console, encryption, and tagging you already use.</p>
<p>The names trip people up, and it's AWS's own fault. Back in late 2021 the class that used to be called plain "S3 Glacier" was renamed to S3 Glacier Flexible Retrieval, and a new faster class, Glacier Instant Retrieval, launched alongside it. So when someone says "just use Glacier," ask which one they mean.</p>
<p>Here is the shape of the three, with S3 Standard-IA thrown in for contrast since it's the class right above them:</p>
<table>
<thead>
<tr>
<th>Class</th>
<th>Lifecycle enum</th>
<th>Retrieval</th>
<th>Min duration</th>
<th>Storage $/GB-month (us-east-1)</th>
</tr>
</thead>
<tbody>
<tr>
<td>S3 Standard-IA</td>
<td><code>STANDARD_IA</code></td>
<td>Milliseconds (direct GET)</td>
<td>30 days</td>
<td>$0.0125</td>
</tr>
<tr>
<td>Glacier Instant Retrieval</td>
<td><code>GLACIER_IR</code></td>
<td>Milliseconds (direct GET)</td>
<td>90 days</td>
<td>$0.004</td>
</tr>
<tr>
<td>Glacier Flexible Retrieval</td>
<td><code>GLACIER</code></td>
<td>1 min to 12 hours</td>
<td>90 days</td>
<td>$0.0036</td>
</tr>
<tr>
<td>Glacier Deep Archive</td>
<td><code>DEEP_ARCHIVE</code></td>
<td>12 to 48 hours</td>
<td>180 days</td>
<td>$0.00099</td>
</tr>
</tbody>
</table>
<p>There's a trap hiding in that enum column. In lifecycle and storage-class APIs, <code>GLACIER</code> means Flexible Retrieval, and <code>GLACIER_IR</code> means Instant Retrieval. Lots of people assume <code>GLACIER</code> is the instant one because it has the shortest name. It isn't. If you write a lifecycle rule that transitions to <code>GLACIER</code> expecting millisecond reads, you'll get a class that needs a multi-hour restore.</p>
<p>One more distinction worth clearing up. There's an older standalone "Amazon Glacier" service built around vaults and a separate API. That is not the same as the S3 Glacier storage classes, and AWS stopped taking new customers for the legacy vault service in December 2025. For anything new, you want the S3 storage classes covered here, not vaults.</p>
<h2 id="how-fast-can-you-retrieve-data-from-each-glacier-class">How fast can you retrieve data from each Glacier class?</h2>
<p>Retrieval speed is the single biggest difference between the three classes, ranging from milliseconds to two days. Glacier Instant Retrieval behaves like S3 Standard: you call <code>GET</code> and the object comes back immediately, no restore step. The other two make you ask first, then wait.</p>
<p>Flexible Retrieval and Deep Archive offer retrieval tiers. You pick a tier when you request a restore, trading money for speed:</p>
<table>
<thead>
<tr>
<th>Class and tier</th>
<th>Typical time</th>
</tr>
</thead>
<tbody>
<tr>
<td>Glacier Instant Retrieval</td>
<td>Milliseconds (no restore)</td>
</tr>
<tr>
<td>Flexible: Expedited</td>
<td>1 to 5 minutes</td>
</tr>
<tr>
<td>Flexible: Standard</td>
<td>3 to 5 hours</td>
</tr>
<tr>
<td>Flexible: Bulk</td>
<td>5 to 12 hours</td>
</tr>
<tr>
<td>Deep Archive: Standard</td>
<td>Within 12 hours</td>
</tr>
<tr>
<td>Deep Archive: Bulk</td>
<td>Within 48 hours</td>
</tr>
</tbody>
</table>
<p>A couple of details that bite people. Deep Archive has no Expedited tier at all, so the fastest you can pull data out is "within 12 hours" on Standard. And Flexible Expedited is quoted at 1 to 5 minutes for objects under 250 MB. Larger objects stream out at high throughput once the job starts, but the start is still near-instant for small ones.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/s3-glacier-instant-vs-flexible-vs-deep-archive-retrieval.svg" alt="Animated chart comparing S3 Glacier retrieval times across Instant Retrieval, Flexible Expedited, Standard, and Bulk, and Deep Archive Standard and Bulk tiers, on a log scale from milliseconds to 48 hours"></p>
<p>There's also a free speed-up most people miss. Since 2023, Flexible Retrieval Standard restores triggered through S3 Batch Operations start returning objects within minutes instead of the usual 3 to 5 hours, at no extra cost. If you're restoring many objects at once, run it as a Batch Operations job rather than a loop of single restore calls.</p>
<h2 id="how-much-does-each-s3-glacier-class-cost">How much does each S3 Glacier class cost?</h2>
<p>Storage is the cheap, obvious number, and retrieval is where the surprises live. Deep Archive stores data for about a quarter of what Instant Retrieval costs, but the order flips when you read data back.</p>
<p>Storage per GB-month in us-east-1:</p>
<ul>
<li>Glacier Instant Retrieval: $0.004</li>
<li>Glacier Flexible Retrieval: $0.0036</li>
<li>Glacier Deep Archive: $0.00099</li>
</ul>
<p>Retrieval cost per GB (the part that catches teams off guard):</p>
<ul>
<li>Instant Retrieval: $0.03 per GB</li>
<li>Flexible Expedited: $0.03 per GB plus $10.00 per 1,000 requests</li>
<li>Flexible Standard: $0.01 per GB</li>
<li>Flexible Bulk: free</li>
<li>Deep Archive Standard: $0.02 per GB</li>
<li>Deep Archive Bulk: $0.0025 per GB</li>
</ul>
<p>Notice that Flexible Bulk retrieval is free, which makes Flexible Retrieval a genuinely cheap place to park data you'll occasionally bulk-restore and can wait half a day for. And Expedited carries a steep per-request fee on top of the per-GB charge, so it's a rush button, not a default.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/s3-glacier-instant-vs-flexible-vs-deep-archive-cost.svg" alt="Animated bar chart comparing S3 Glacier storage cost per GB-month against retrieval cost per GB for Instant Retrieval, Flexible Retrieval, and Deep Archive, showing that the cheapest class to store is not the cheapest to read back"></p>
<p>Here's a worked example to make the tradeoff concrete. Say you archive 10 TB and keep it for a year, then restore 200 GB once:</p>
<ul>
<li><strong>Deep Archive:</strong> storage is 10,000 GB times $0.00099 times 12 months, about $119 for the year. A 200 GB Standard restore is 200 times $0.02, so $4 plus small request fees. Cheap to hold, cheap to read once, but you wait up to 12 hours.</li>
<li><strong>Glacier Instant Retrieval:</strong> storage is 10,000 GB times $0.004 times 12 months, about $480 for the year. Retrieval is a plain GET with no restore wait.</li>
</ul>
<p>The storage gap is roughly 4x. If you read that data a handful of times a year and can't wait hours each time, the extra $360 a year is what instant access costs you. If you genuinely never touch it, that 4x is pure waste and Deep Archive wins easily.</p>
<h2 id="what-minimum-durations-and-hidden-fees-should-you-watch-for">What minimum durations and hidden fees should you watch for?</h2>
<p>Every Glacier class bills a minimum storage duration, and deleting early doesn't save you money. Instant and Flexible Retrieval both charge a 90-day minimum, Deep Archive charges 180 days, and Standard-IA charges 30 days. If you delete, overwrite, or transition an object before its minimum, AWS charges a prorated fee for the remaining days as if the object had stayed.</p>
<p>The fee that surprised me most was per-object overhead. For Flexible Retrieval and Deep Archive, every archived object carries 40 KB of extra metadata: 32 KB billed at the Glacier rate and 8 KB billed at the S3 Standard rate. That's nothing for a 5 GB backup. It's brutal for ten million 4 KB files, where the overhead dwarfs the actual data. The fix is to aggregate small files into archives (tar, zip, or a columnar format) before they land in Glacier. Glacier Instant Retrieval skips this 40 KB model but bills a 128 KB minimum object size instead, so tiny objects are still a bad fit there.</p>
<p>One more cost that hides until restore day: while a Flexible or Deep Archive object is restored, you pay for both copies at once. The archived object keeps billing at its Glacier rate, and the temporary restored copy bills at the S3 Standard rate for the whole window you asked to keep it available. Ask for 30 days of availability on a big restore and that temporary copy is a real line item.</p>
<h2 id="how-do-s3-lifecycle-rules-move-objects-into-glacier">How do S3 Lifecycle rules move objects into Glacier?</h2>
<p>S3 Lifecycle rules transition objects between classes automatically based on age, so you rarely change storage classes by hand. You attach a rule to a bucket or prefix, and S3 moves objects down the chain as they cross the day thresholds you set.</p>
<p>Transitions only flow one direction, from hotter to colder. Standard can go to any colder class, Flexible Retrieval can only move on to Deep Archive, and Deep Archive is the end of the line. To move data back up, you restore a copy and rewrite it with the new class. There's also a floor: objects must sit in Standard for at least 30 days before a rule can move them to Standard-IA or One Zone-IA, though you can transition straight to a Glacier class from day one.</p>
<p>A typical "age out the logs" rule looks like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">Rules</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">ID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">archive-logs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Filter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">Prefix</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">logs/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Transitions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">Days</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">StorageClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">STANDARD_IA</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">Days</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 90</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">StorageClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">GLACIER</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">Days</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 180</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">StorageClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">DEEP_ARCHIVE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      ]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  ]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Apply it with the CLI:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">aws</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> s3api</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> put-bucket-lifecycle-configuration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --bucket</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-archive-bucket</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --lifecycle-configuration</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> file://lifecycle.json</span></span></code></pre></figure>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/s3-glacier-instant-vs-flexible-vs-deep-archive-lifecycle.svg" alt="Animated waterfall diagram showing an S3 object aging through storage classes over time: S3 Standard on day zero, Standard-IA at day 30, Glacier Flexible Retrieval at day 90, and Deep Archive at day 180, with a data object flowing down the chain"></p>
<p>Two details save real money here. Remember the 90 and 180-day minimums when you set thresholds, because transitioning an object to Deep Archive on day 95 and deleting it on day 120 still bills the full 180 days. And as of September 2024, S3 no longer transitions objects smaller than 128 KB by default, so a bucket full of tiny files may quietly skip the rule unless you override the size filter. If you want this whole setup in version control instead of the console, define the bucket and its lifecycle rules with <a href="https://www.rabinarayanpatra.com/blogs/create-your-first-cloudformation-stack">AWS CloudFormation</a> so the policy ships with your infrastructure.</p>
<h2 id="how-do-you-restore-an-archived-object-from-glacier">How do you restore an archived object from Glacier?</h2>
<p>For Flexible Retrieval and Deep Archive, you POST a restore request, wait for the tier's retrieval time, then download a temporary copy. Instant Retrieval skips all of this, since a normal GET works directly.</p>
<p>The restore call names how many days the temporary copy should stay available and which tier to use:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">aws</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> s3api</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> restore-object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --bucket</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-archive-bucket</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --key</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> logs/2026/app.log.gz</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --restore-request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{"Days":7,"GlacierJobParameters":{"Tier":"Standard"}}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span></code></pre></figure>
<p>While the restore runs and after it finishes, the object's storage class stays <code>GLACIER</code> or <code>DEEP_ARCHIVE</code>. The restore doesn't move the object, it just creates a readable copy next to it. You check progress with <code>head-object</code>, which reports <code>Restore: ongoing-request="true"</code> while the job runs and flips to <code>false</code> with an expiry date once the copy is ready:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">aws</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> s3api</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> head-object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --bucket</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-archive-bucket</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --key</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> logs/2026/app.log.gz</span></span></code></pre></figure>
<p>When the number of <code>Days</code> you asked for runs out, S3 deletes the temporary copy and you're back to just the archived object. If you actually want the data to stay hot, copy the restored object to a non-archive class like Standard or Standard-IA before the window closes. This restore model is unique to the Glacier classes, and it's one more reason the <a href="https://www.rabinarayanpatra.com/blogs/aws-s3-2026-files-vectors-and-beyond">broader S3 feature set</a> is worth understanding before you commit data to cold storage.</p>
<h2 id="which-s3-glacier-class-should-you-actually-pick">Which S3 Glacier class should you actually pick?</h2>
<p>Match the class to how often you read the data and how long you can wait, not to the storage price alone. The three classes map cleanly onto three access patterns:</p>
<ul>
<li><strong>Glacier Instant Retrieval:</strong> you access the data about once a quarter, but when you do you need it in milliseconds. Think medical images, news media archives, or user files that are rarely opened but must load instantly when they are. You pay roughly 4x the Deep Archive storage rate for that immediacy.</li>
<li><strong>Glacier Flexible Retrieval:</strong> you read the data once or twice a year and can wait minutes to hours. Backups, large datasets you reprocess occasionally, anything where a Bulk restore (free, up to 12 hours) is fine. This is the practical default for most archives.</li>
<li><strong>Glacier Deep Archive:</strong> you almost never read the data and you're keeping it for compliance, legal hold, or long-term disaster recovery. You can wait up to 48 hours, and the 180-day minimum is irrelevant because the data lives for years.</li>
</ul>
<p>The mistake I see most often is reaching for Deep Archive purely because the storage number is smallest. If there's any real chance you'll need the data inside a day, that choice turns a five-minute task into a next-day one, and the cheap storage line never makes up for the operational pain.</p>
<h2 id="what-should-you-check-before-moving-data-to-glacier">What should you check before moving data to Glacier?</h2>
<p>Model the retrieval before you write the lifecycle rule, not after. Storage cost is the number everyone compares, but it's the cheap, visible one. Retrieval cost and retrieval time are invisible right up until the day you actually need the data, which tends to be the worst possible day to discover that a restore takes 48 hours or costs more than you expected.</p>
<p>My rule of thumb: if I can't say out loud how I'll get the data back, how fast that will be, and what it costs, the data isn't ready for Glacier yet. Write down the realistic restore scenario (how much, how often, how fast) and let that pick the class. Do that, and S3 Glacier goes from a billing trap to one of the best deals in cloud storage.</p>
<p>For the exact figures, see the AWS docs on <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html">S3 storage classes</a>, the <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/glacier-storage-classes.html">S3 Glacier storage classes guide</a>, the <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects-retrieval-options.html">retrieval options reference</a>, and the live <a href="https://aws.amazon.com/s3/pricing/">Amazon S3 pricing page</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/aws-s3-2026-files-vectors-and-beyond">Amazon S3 Files: AWS Just Turned Object Storage Into a File System</a>. How S3 keeps growing past plain object storage, the same service these Glacier classes live in.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/create-your-first-cloudformation-stack">How to Create Your First AWS CloudFormation Stack (2026)</a>. Put your bucket and lifecycle rules in version-controlled infrastructure instead of clicking through the console.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/what-is-infrastructure-as-code">What Is Infrastructure as Code? A Beginner's Guide for 2026</a>. The foundation for managing S3 lifecycle policies and storage classes as code.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Add Custom MCP Tools to Your Slack AI Bot]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-add-mcp-tools-slack-ai-bot</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-add-mcp-tools-slack-ai-bot</guid>
      <pubDate>Thu, 18 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[How to extend a Slack AI bot driven by Codex, Claude Code, or Cursor with custom MCP tools: build a FastMCP server in Python, wire it per-agent, gate destructive calls behind Slack approvals.]]></description>
      <content:encoded><![CDATA[<p>A Slack bot wired to a CLI agent already gets you shell, file editing, and web search out of the box. The moment you point it at the rest of your stack, it becomes the only chat surface you need. <code>is order 9182 still in payment_pending?</code> answers from your read-only Postgres replica. <code>add a 30-min sync with Prem next Tuesday afternoon</code> writes to your calendar. <code>pull the price for this Amazon listing</code> runs a small scraper. None of that involves changing the Slack glue script, the agent CLI binary, or even restarting the bot.</p>
<p>The reason that works is the Model Context Protocol. Every modern agent CLI ships as an MCP client, and any process that speaks JSON-RPC 2.0 over stdio becomes a tool source. This post is the companion to the <a href="https://www.rabinarayanpatra.com/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent">Slack AI bot setup guide</a> and walks through building, wiring, and operating custom MCP servers for that bot, regardless of whether the agent on the other end is OpenAI Codex, Anthropic Claude Code, or Cursor.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-add-mcp-tools-slack-ai-bot.webp" alt="Diagram of a CLI agent acting as an MCP client. The agent sits in the middle, with arrows pointing out to four separate MCP server processes: a Postgres read-only server, a calendar server, a Notion server, and a custom internal-API server. Each server speaks JSON-RPC 2.0 over stdio." width="1600" height="900"></p>
<p>What you get by the end: a fifteen-line Python MCP server that exposes two tools, three short config snippets that wire it into Codex, Claude Code, and Cursor respectively, and an operational pattern that makes the bot safe to share with a small team. The MCP server you write is identical across agents; only the registration file changes.</p>
<h2 id="why-add-custom-mcp-tools-when-the-agent-already-ships-with-shell-and-web_search">Why add custom MCP tools when the agent already ships with shell and web_search?</h2>
<p>Because shell is the worst possible interface for everything except shell. Asking the agent to figure out which Postgres column to filter on, write the right <code>psql</code> invocation, and parse the result every time you want to look up an order is a recipe for slow runs, hallucinated column names, and the occasional accidentally dropped table. A purpose-built <code>query_orders(status)</code> tool is one line of Python on your side, runs in milliseconds, and is impossible for the model to misuse.</p>
<p>The deeper reason is shape. A tool description tells the LLM what arguments to send and what kind of result to expect. A tool runs deterministically on your hardware with your credentials and your access rules. Together, those two facts mean the agent stops guessing about your internal systems and starts behaving like a thin orchestration layer over functions you have already tested. The model picks the right tool, fills in the arguments, and reads the output. You wrote the function in the language you already use.</p>
<p>Production teams that have used MCP for a few months almost all converge on the same pattern. Wrap read-only paths to internal APIs as MCP tools. Wrap a handful of write paths but gate them behind approvals. Leave everything else to shell. The Slack bot becomes a fast read interface over the systems where reading is cheap and writing is risky, and that is exactly what most chat-driven workflows actually need.</p>
<h2 id="what-is-mcp-and-how-does-an-agent-cli-talk-to-mcp-servers">What is MCP and how does an agent CLI talk to MCP servers?</h2>
<p>Model Context Protocol is an open spec that defines a JSON-RPC 2.0 conversation between an MCP client and one or more MCP servers. Anthropic published the original spec, and OpenAI's Codex CLI, Anthropic's Claude Code, and Cursor have all implemented the client side. The protocol itself is small: a server advertises a tool catalog with names, descriptions, and JSON schemas, and the client invokes tools by name with arguments.</p>
<p>For any of these CLIs, MCP servers live as child processes that the agent spawns over stdio at session start. The wire format is JSON-RPC 2.0, one message per line, with method names like <code>tools/list</code> and <code>tools/call</code>. From inside the agent session, every MCP tool shows up next to the built-in tools, and the LLM picks whichever one fits the prompt. You do not have to teach the model what your tool does; the docstring you wrote becomes the description it reads.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-add-mcp-tools-slack-ai-bot-lifecycle.webp" alt="Tool call lifecycle diagram. A user asks &#x22;is order 9182 still pending?&#x22; in Slack. The Slack glue forwards the prompt to a CLI agent. The agent consults its tool catalog, picks query_orders, sends a tools/call JSON-RPC request to the MCP server process. The server returns the row over stdout. The agent composes a natural-language reply and streams it back through the glue into the same Slack message." width="1600" height="900"></p>
<p>A few constraints worth knowing before you write anything. MCP servers run as separate processes for a reason: they isolate your tool dependencies from the agent runtime, let you write servers in any language with a JSON-RPC library, and survive a crash without taking the agent down. The agent spawns them on demand and keeps the standard-streams open for the duration of the session. The only thing you ship is the executable and the config that points the agent at it.</p>
<h2 id="how-do-you-build-a-minimal-mcp-server-in-15-lines-of-python">How do you build a minimal MCP server in 15 lines of Python?</h2>
<p>Install the official Python SDK, write two decorated functions, call <code>mcp.run()</code>. That is the entire scaffold. The SDK auto-generates JSON Schema from your type hints and uses the docstring as the tool description, so a clean signature is the entire UI for the LLM. The same server binary works under any MCP-compliant client.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">pip</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mcp[cli]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>Save this as <code>~/my_tools/mcp_server.py</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">my_tools/mcp_server.py - two-tool MCP server.</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">fastmcp </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> FastMCP</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">mcp </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> FastMCP</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">my-tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">@</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">tool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> get_weather</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">city</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> -></span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">    """</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">Current weather for a city. Returns a one-line string.</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    # ...call your weather API here</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'sunny in </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">city</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">, 24C'</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">@</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">tool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> list_my_tickets</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">open</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">    """</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">List tickets I own. status: open | closed | all. Defaults to open.</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    # ...query your ticket system here</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">TKT-101</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">TKT-205</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> __name__</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">__main__</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">      # speaks JSON-RPC over stdin/stdout</span></span></code></pre></figure>
<p>That is a working MCP server. Tool names, descriptions, and input schemas are derived from the Python signatures and docstrings. Test it independently of any agent with a quick echo from another shell:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">echo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{"jsonrpc":"2.0","id":1,"method":"tools/list"}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> python</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/my_tools/mcp_server.py</span></span></code></pre></figure>
<p>You should see a JSON-RPC response listing <code>get_weather</code> and <code>list_my_tickets</code> with their full schemas. If that works, the server is ready to wire into whichever agent you are running.</p>
<p>Two small details that catch first-timers. The <code>mcp.run()</code> function uses stdio transport by default, which is what every CLI agent expects. If you instead try <code>mcp.run(transport='streamable-http')</code> you switch to the HTTP transport for remote MCP servers, which is a different config story and not what we want for a local Slack bot. And anything you <code>print()</code> from your tools goes to stdout, which is the wire. Use <code>logging</code> with a <code>StreamHandler</code> pointed at stderr instead, or you will corrupt the JSON-RPC stream and watch the agent disconnect.</p>
<h2 id="how-do-you-wire-the-same-mcp-server-into-codex-claude-code-and-cursor">How do you wire the same MCP server into Codex, Claude Code, and Cursor?</h2>
<p>Each agent CLI reads MCP server definitions from its own config file, but the entry shape is conceptually the same: a name, a command, and optional args and env. The server binary stays put; only the three registration files change.</p>
<p>For <strong>OpenAI Codex CLI</strong>, add a table to <code>~/.codex/config.toml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="toml" data-theme="material-theme github-light"><code data-language="toml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># ~/.codex/config.toml</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">mcp_servers</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">my-tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">python</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">args </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/Users/you/my_tools/mcp_server.py</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">startup_timeout_sec </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 10</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">tool_timeout_sec </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 60</span></span></code></pre></figure>
<p>Or use the CLI-managed alternative:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">codex</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> mcp</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> add</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-tools</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> python</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /Users/you/my_tools/mcp_server.py</span></span></code></pre></figure>
<p>For <strong>Anthropic Claude Code</strong>, drop a <code>.mcp.json</code> at the project root (or anywhere you point Claude with <code>--mcp-config &#x3C;file></code>):</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">mcpServers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">my-tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">python</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/Users/you/my_tools/mcp_server.py</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>For <strong>Cursor</strong>, the file is <code>.cursor/mcp.json</code> in the project root, with the same JSON shape:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">mcpServers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">my-tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">python</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/Users/you/my_tools/mcp_server.py</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>A few config options I reach for almost every time on the Codex side. <code>startup_timeout_sec</code> defaults to ten seconds, which is tight if your server hits a database on import; bump it to thirty if you see startup timeouts. <code>tool_timeout_sec</code> defaults to sixty and is the per-call timeout for any single tool invocation; raise it for tools that touch slow downstream APIs but never set it to infinity because that converts a stuck call into a hung agent. <code>env</code> and <code>env_vars</code> together let you pass secrets without inlining them in the args list, which matters because the args show up in every Codex log line. Claude Code and Cursor expose similar timeout and env knobs in their JSON config; consult their respective references for the exact key names.</p>
<p>After the config change, run <code>codex mcp list</code> (Codex) or restart your <code>claude -p</code> / <code>cursor-agent -p</code> invocation to confirm the new server is picked up. A successful wire-up shows your two tool names in the response alongside the built-in tools. From inside the Slack bot, no glue-script change is needed: the moment the agent picks up the new server on its next spawn, the bot can use the tools.</p>
<h2 id="what-custom-mcp-tools-should-you-build-first-for-a-slack-ai-bot">What custom MCP tools should you build first for a Slack AI bot?</h2>
<p>The high-value tools are the boring ones. Anything you currently look up by opening a dashboard tab, copy-pasting a SQL query, or running a curl command three times a week is a candidate. The shape of the function does not matter; the description and the schema do.</p>
<p>A short list of tools that consistently pay off on day one of a self-hosted Slack agent:</p>
<table>
<thead>
<tr>
<th>Tool category</th>
<th>Example tool signatures</th>
<th>Why it earns its keep in Slack</th>
</tr>
</thead>
<tbody>
<tr>
<td>Read-only database</td>
<td><code>query_orders(status)</code>, <code>lookup_user(email)</code></td>
<td>Answers ops questions like "are there any pending orders for this account?" in seconds.</td>
</tr>
<tr>
<td>Calendar and tasks</td>
<td><code>list_events(window)</code>, <code>create_event(title, when, with)</code></td>
<td>"Schedule a 30-min sync with Prem next Tuesday afternoon" lands on the calendar without leaving Slack.</td>
</tr>
<tr>
<td>Internal API wrapper</td>
<td><code>get_inventory(sku)</code>, <code>cancel_order(id, reason)</code></td>
<td>Bot becomes the safest interface to your APIs because every call is logged and gated.</td>
</tr>
<tr>
<td>Note systems</td>
<td><code>search_notes(query)</code>, <code>append_to_note(title, body)</code></td>
<td>"Add this finding to my project notes" works from any thread without app switching.</td>
</tr>
<tr>
<td>Site scrapers</td>
<td><code>scrape_amazon_listing(url)</code>, <code>fetch_pdf_text(url)</code></td>
<td>One-liner replacements for the read-only side of headless-browser scripts you already run locally.</td>
</tr>
<tr>
<td>Home automation</td>
<td><code>set_thermostat(c)</code>, <code>turn_on_lights(room)</code></td>
<td>A Home Assistant MCP server makes the bot a smart-home interface that travels with Slack.</td>
</tr>
</tbody>
</table>
<p>Two principles I keep coming back to. Treat tools as pure functions where possible, idempotent and side-effect-free, because the model will call them eagerly when the prompt sounds even vaguely related. Validate every argument inside the function body and raise with a clear message on bad input, because the JSON Schema is advisory and the LLM does occasionally hallucinate a value that does not match the type hint. Errors propagate back to the model as text, so a sentence that explains what went wrong is often enough for the model to retry with a corrected argument or back off cleanly.</p>
<h2 id="how-do-you-gate-destructive-mcp-tools-behind-slack-approve-and-deny-buttons">How do you gate destructive MCP tools behind Slack Approve and Deny buttons?</h2>
<p>Combine the agent-level approval flag with a per-server policy in the agent's config. The approval request flows through the same JSON event stream as approvals for built-in tools like <code>shell</code>, so the existing Approve and Deny block in your Slack bot picks them up without any code changes.</p>
<p>On <strong>Codex</strong>, the config delta is one line per server:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="toml" data-theme="material-theme github-light"><code data-language="toml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">mcp_servers</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">my-tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">python</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">args </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/Users/you/my_tools/mcp_server.py</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">default_tools_approval_mode </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">on-request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>Then run the agent with <code>--ask-for-approval on-request</code>. You can also set the global <code>approval_policy = "on-request"</code> at the top of <code>config.toml</code>, which makes every tool ask before running.</p>
<p>On <strong>Claude Code</strong>, gate sensitive tools through the CLI permission flags instead. Run with <code>--permission-mode acceptEdits</code> to let safe file edits through without prompting, and use <code>--allowedTools "Bash(git diff *),Read,Edit"</code> to constrain what the agent can do without an approval prompt. Anything outside the <code>--allowedTools</code> allowlist triggers a permission request, which your Slack glue can intercept the same way it does for Codex.</p>
<p>On <strong>Cursor</strong>, leave <code>--approve-mcps</code> off (the default) for any server that exposes write tools. Cursor will prompt for approval on each tool call, and the prompt becomes a button block in Slack. For the read-only <code>query_orders</code> server you can pass <code>--approve-mcps</code> to skip the prompt and let queries fly.</p>
<p>The companion post on the <a href="https://www.rabinarayanpatra.com/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent">Slack AI bot setup</a> covers the Slack-side handler that posts the buttons and writes the decision back to the agent; the MCP side does not need any extra code.</p>
<p>For tools that are clearly safe, like read-only queries, you can let them run without prompting on every agent. The mental model is: every write or external mutation goes through the Slack-button gate, every read goes through automatically. That is the right default for most teams. Once you have a few thousand approved tool calls of history, you can decide whether to relax specific tools to auto-approve based on observed patterns.</p>
<p>If you want per-tool control finer than the per-server default, split the server into two: one server for the auto-approve tools, one for the gated ones, each with its own approval policy in the relevant config file. It is more files but cleaner config, and it makes the policy obvious to anyone reading the JSON or TOML.</p>
<h2 id="what-production-traps-will-bite-you-with-mcp-servers-in-a-slack-bot">What production traps will bite you with MCP servers in a Slack bot?</h2>
<p>The traps cluster around three sharp edges: stdio hygiene, startup latency, and approval scoping. The fixes are short, but they all show up the first time you run a real workload.</p>
<p>Stdio hygiene comes first because it breaks the loudest. Every <code>print</code> call from your server lands on the agent's stdin and corrupts the JSON-RPC stream, which then disconnects the server mid-session. Configure Python <code>logging</code> with a <code>StreamHandler(sys.stderr)</code> at module load and never call <code>print()</code> from inside a tool. Catch broad exceptions inside each tool and return a sensible error string rather than letting an uncaught exception write a traceback to stdout.</p>
<p>Startup latency adds up when you wire several MCP servers into the same config. Most agents spawn every configured server on session start, so a server that imports SQLAlchemy and opens a database connection at module load adds a second to every CLI invocation. Defer the heavy imports to inside the tool functions, or use <code>startup_timeout_sec</code> (Codex) and the equivalent in Claude Code or Cursor to raise the budget for the slow servers and accept the latency hit. For the Slack bot, a quick spawn is what makes the experience feel snappy, so prefer lazy initialization over upfront connection pools.</p>
<p>Approval scoping is the third trap. The per-server approval policy applies to every tool that server exposes. If you wired a read-only server and a write server under the same registration, every read goes through the same approval flow as every write, and your users will rage-click Approve. Split servers along the read-write axis from day one. It is one extra config entry per server and it makes the operational story honest.</p>
<p>A few smaller details that earn their keep. Treat the MCP server as a deployable artifact and pin its version in <code>requirements.txt</code>, because the JSON-RPC method names have shifted across MCP spec revisions and breaking changes propagate through the client side of the wire. Keep one shared <code>mcp.tool()</code> decorator registry per server so the LLM never sees duplicate tool names. Log every tool call to a file with timestamp, user, tool name, and arguments, because the audit trail is what makes the bot safe to share beyond yourself.</p>
<h2 id="where-does-this-leave-the-slack-ai-bot">Where does this leave the Slack AI bot?</h2>
<p>A bot wired to a CLI agent is already useful. The same bot wired to half a dozen carefully chosen MCP tools is a different category of useful, because every question that used to require switching to a dashboard tab now answers in the same thread you were already in. The pattern scales from one tool to twenty without adding any Slack code, because the LLM does the dispatch and the glue is invisible. And because MCP is agent-agnostic, the work you do here outlives any one CLI: if you switch from Codex to Claude Code or Cursor next quarter, the server stays put, only the registration file changes.</p>
<p>If you stopped at the basic Slack bot in the <a href="https://www.rabinarayanpatra.com/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent">companion post</a>, the next move is one read-only MCP tool for whichever system you find yourself logging into the most. Pick the boring one. Wrap one read path. Wire it into the relevant config file for the agent you are running. Watch your Slack thread answer a question that used to take three tab switches. The moment that lands, the rest of the tool catalog writes itself.</p>
<p>For more on the protocol and ecosystem, see the <a href="https://modelcontextprotocol.io/">MCP specification site</a>, the <a href="https://github.com/modelcontextprotocol/python-sdk">Python MCP SDK on GitHub</a>, and the <a href="https://developers.openai.com/codex/mcp">Codex MCP configuration reference</a>. For the broader Slack glue side that this post builds on, the companion guide is linked below.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent">How to Build a Self-Hosted Slack AI Bot with any CLI Agent</a>. The base bot this post extends, including Socket Mode setup, the basic glue, the approval flow, and the systemd or launchd service config.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/build-stateless-mcp-server-2026-07-28-spec">How to Build a Stateless MCP Server for the 2026-07-28 Spec</a>. A deeper look at the MCP server side, covering the newest stateless transport that future agent versions will speak.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/build-mcp-app-interactive-ui">How to Build an Interactive MCP App with the MCP Apps SDK</a>. What changes when an MCP server hosts an interactive UI rather than only tool functions, useful context for richer Slack workflows.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Migrate Vercel AI SDK 5 to 6: Breaking Changes Guide]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/vercel-ai-sdk-5-to-6-migration-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/vercel-ai-sdk-5-to-6-migration-guide</guid>
      <pubDate>Tue, 16 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Step-by-step Vercel AI SDK 5 to 6 migration. Codemod walkthrough, useChat parts model, ToolLoopAgent replacement for streamText, and tool approval.]]></description>
      <content:encoded><![CDATA[<p>Vercel shipped AI SDK 6 on May 7, 2026. It is the biggest breaking-change release the SDK has shipped since the 3.0 split. The v3 Language Model Specification, a real Agent abstraction, human-in-the-loop tool approval, a unified message-parts model on useChat, and a new streaming wire format all landed in the same drop.</p>
<p>I migrated the AI chatbot on this portfolio over the weekend. The codemod did most of the mechanical work, but four spots needed hand edits. This post walks the migration the way I ran it, with the exact code diffs that broke and the verification steps that confirmed the migration shipped without a silent UX regression.</p>
<h2 id="what-changed-in-vercel-ai-sdk-6">What changed in Vercel AI SDK 6?</h2>
<p>AI SDK 6 ships the v3 Language Model Specification, the Agent interface with ToolLoopAgent as the production implementation, a unified message-parts array on useChat, an OAuth-ready MCP client, reranking, image editing, and a <a href="https://www.rabinarayanpatra.com/snippets/nextjs/streaming-suspense-loading">new streaming wire format that pairs naturally with Suspense and <code>loading.tsx</code></a> for a 15 to 25 percent first-token latency win on cold-start route invocations. The Vercel post calls the wire format change "the foundation for several capabilities Vercel has been signposting for the second half of 2026".</p>
<p>The four breaking-change axes that affect existing apps are:</p>
<table>
<thead>
<tr>
<th>Axis</th>
<th>What broke</th>
<th>Where you fix it</th>
</tr>
</thead>
<tbody>
<tr>
<td>useChat message-parts</td>
<td>Messages no longer carry a flat <code>content</code> string. They carry a <code>parts</code> array of typed entries.</td>
<td>Every React component that renders <code>message.content</code></td>
</tr>
<tr>
<td>Tool-call streaming lifecycle</td>
<td>Tool calls now flow through explicit <code>state</code> transitions (<code>input-streaming</code>, <code>input-available</code>, <code>output-available</code>, <code>approval-requested</code>).</td>
<td>Tool render switch statements in your UI</td>
</tr>
<tr>
<td>Provider-adapter contract</td>
<td>The v3 Language Model Specification changes how provider packages expose tools, finish reasons, and usage details.</td>
<td>Custom provider wrappers and middleware</td>
</tr>
<tr>
<td>Streaming wire format</td>
<td>New ordering and chunk types for tool calls and reasoning traces.</td>
<td>Anything that reads the SSE stream directly</td>
</tr>
</tbody>
</table>
<p>The codemod handles the easy half. The other half is where this guide focuses.</p>
<h2 id="which-breaking-changes-hit-you-first">Which breaking changes hit you first?</h2>
<p>The useChat parts model is the change that hits you first because it is silent. Your app keeps compiling. Your messages array keeps populating. But the moment a tool gets invoked, the tool-call UI never renders, because your code is still reading <code>message.content</code> and the tool call now lives in <code>message.parts</code>.</p>
<p>I caught this in my own portfolio because the AI chat that answers questions about my blog posts uses one tool (<code>search_blog_posts</code>). After upgrading, the chat worked perfectly for plain prompts and dropped the search-result card entirely for tool prompts. No console error, no broken request. Just a missing block in the UI.</p>
<p>The second-hardest break is <code>streamText</code> callers that built their own agent loop with <code>stopWhen</code> and a step counter. The v6 way is <code>ToolLoopAgent</code>, and the two cannot be mixed in a single component because they emit different stream shapes.</p>
<p>The third break is the wire format. If you parse the SSE stream by hand anywhere in your codebase, you have to rewrite that parser. Most apps do not, but server-rendered chat UIs and custom telemetry layers usually do.</p>
<h2 id="how-do-you-run-the-codemod-the-right-way">How do you run the codemod the right way?</h2>
<p>You run the codemod by pinning the SDK version first, running the official transform from a clean working tree, then reviewing every changed file before you let it land. Treat the codemod output as a starting point, not a finished migration.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 1. Pin both packages at the v6 line. Do this before running the codemod.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ai@6</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @ai-sdk/openai@6</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @ai-sdk/anthropic@6</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 2. Confirm a clean git status. The codemod will rewrite files in place.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> status</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 3. Run the official codemod from the repo root.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @ai-sdk/codemod</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> upgrade</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> v6</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 4. Inspect the diff.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> diff</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --stat</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> diff</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> src/app</span></span></code></pre></figure>
<p>The codemod prints a summary of files touched. On the chatbot in this portfolio it rewrote 9 files: the route handler, the chat component, and seven smaller utilities. It did not touch my custom tool renderer or the route that streams blog embedding search results, because both rely on shapes that the codemod cannot infer.</p>
<p>If the diff looks plausible, commit it as a separate commit before touching anything else. That gives you a clean rollback point.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> add</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -A</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> commit</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">chore(ai-sdk): apply v6 codemod automated changes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>Now you can start the hand edits on top of a known-good base.</p>
<h2 id="how-do-you-migrate-usechat-to-message-parts">How do you migrate useChat to message parts?</h2>
<p>You migrate useChat by rewriting every render path that reads <code>message.content</code> to walk <code>message.parts</code> and switch on <code>part.type</code>. The new model treats text, tool calls, tool results, and reasoning traces as first-class entries that all live in the same array.</p>
<p>Here is the before and after on the simplest possible chat component.</p>
<p><strong>v5 style (broken in v6 the moment a tool fires):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">use client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> useChat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai/react</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Chat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> handleSubmit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> handleInputChange</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> useChat</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">strong</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">strong</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      ))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">form</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> onSubmit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">handleSubmit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">input</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} </span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1">onChange</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">handleInputChange</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">form</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>v6 style with the parts model:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">use client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> useState</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">react</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> useChat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai/react</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> WeatherAgentUIMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/agents/weather-agent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Chat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> sendMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> useChat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">WeatherAgentUIMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> setInput</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">''</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#6F42C1"> onSubmit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> React</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">FormEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">preventDefault</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">trim</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">return</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    sendMessage</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    setInput</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">''</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">strong</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">strong</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">parts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            switch</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">type</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">              case</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">              case</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tool-weather</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">WeatherCard</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} </span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1">invocation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">              case</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">reasoning</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Reasoning</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} </span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1">trace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">              default</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      ))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">form</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> onSubmit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">onSubmit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">input</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} </span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1">onChange</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> setInput</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">value)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">form</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Three things make this work. First, <code>useChat&#x3C;WeatherAgentUIMessage>()</code> infers the typed part union from your agent definition. The exported <code>InferAgentUIMessage&#x3C;typeof weatherAgent></code> is what you import as <code>WeatherAgentUIMessage</code>. Second, <code>handleSubmit</code> and <code>handleInputChange</code> are gone in v6. You manage the input state yourself and call <code>sendMessage({ text })</code> to submit. Third, you need a <code>default</code> fallback in the switch. If you forget it and the agent introduces a new part type, the UI silently drops that block at runtime.</p>
<p>After this change, both the plain-text path and the typed <code>tool-weather</code> path render. The Vercel migration playbook calls this "the silent UX regression" and recommends adding a unit test that asserts every part type your agent emits has a render case.</p>
<h2 id="how-do-you-replace-streamtext-with-toolloopagent">How do you replace streamText with ToolLoopAgent?</h2>
<p>You replace streamText by extracting the loop configuration into a <code>ToolLoopAgent</code> instance and calling <code>.stream()</code> or <code>.generate()</code> on it. The agent holds the model, instructions, tools, and stop condition once, and you reuse it everywhere from chat routes to background jobs to standalone scripts.</p>
<p>The before and after on a typical route handler looks like this.</p>
<p><strong>v5 route handler:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// app/api/chat/route.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> streamText</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> openai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@ai-sdk/openai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/tools/weather</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> streamText</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> openai</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">gpt-4o</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    system</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">You are a helpful weather assistant.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> weather</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    maxSteps</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 20</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toDataStreamResponse</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>v6 route handler with ToolLoopAgent:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// agents/weather-agent.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ToolLoopAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> type</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> InferAgentUIMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/tools/weather</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> weatherAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ToolLoopAgent</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">openai/gpt-5.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  instructions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">You are a helpful weather assistant.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  tools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    weather</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> type</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> WeatherAgentUIMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> InferAgentUIMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">typeof</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// app/api/chat/route.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/agents/weather-agent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> req</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toUIMessageStreamResponse</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Three details matter on this migration. First, the plain provider-prefixed model string <code>'openai/gpt-5.4'</code> routes through the Vercel AI Gateway by default, which is the pattern the Vercel knowledge update calls out as the recommended path. The gateway gives you per-request usage tracking, failover across providers, and zero data retention. The v5-era pattern of importing the provider package and calling <code>openai('...')</code> still works, but the string form is shorter and is what new code should use.</p>
<p>Second, <code>toUIMessageStreamResponse</code> is the v6 name for what used to be <code>toDataStreamResponse</code>. The codemod renames it most of the time. Check the route handlers.</p>
<p>Third, <code>stopWhen</code> lives on the agent now, not on the call. The default is 20 steps. If you used to pass <code>maxSteps: 5</code> everywhere, set <code>stopWhen: stepCountIs(5)</code> on the agent constructor.</p>
<p>I wired the same pattern into the <a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway deep dive</a> on this site. The shape is identical for Claude (<code>'anthropic/claude-sonnet-4.5'</code>) and for any other provider in the gateway catalog.</p>
<h2 id="how-do-you-wire-human-in-the-loop-tool-approval">How do you wire human-in-the-loop tool approval?</h2>
<p>You wire approval by adding a <code>needsApproval</code> predicate to the tool and rendering an <code>approval-requested</code> state in the UI. The agent pauses, the UI shows the approval prompt, and the user accepts or rejects before the tool actually runs.</p>
<p>The tool side looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// tools/run-command.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">zod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> runCommand</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> tool</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Run a shell command on the user machine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  inputSchema</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">string</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">describe</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">The shell command to execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">  needsApproval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">startsWith</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">rm </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">  execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> execCommand</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>The UI side adds a render case for the new state.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">case</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tool-runCommand</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#6F42C1">  if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part.state === </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">approval-requested</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">approval-prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Run: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">code</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">code</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">button</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1">          onClick</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">            addToolApprovalResponse</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">              id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">approval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">              approved</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        ></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">          Approve</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">button</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">button</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1">          onClick</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">            addToolApprovalResponse</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">              id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">approval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">              approved</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        ></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">          Reject</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">button</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#6F42C1">  if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part.state === </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">output-available</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">pre</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">part</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">output</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">pre</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  return null</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>addToolApprovalResponse</code> ships from the <code>useChat</code> return value. The agent resumes execution as soon as the approval message hits the stream. If the user rejects, the agent gets a structured rejection event and can recover or apologize.</p>
<p>The predicate runs on the server, so it can hit a policy service or an authorization check. That is the path I use to gate shell-style tools behind a per-account permission check. The same pattern would gate the more dangerous Anthropic and OpenAI provider tools (<code>anthropic.tools.memory_20250818</code>, <code>openai.tools.shell</code>) when those land in your stack.</p>
<h2 id="how-do-you-verify-the-migration-before-shipping">How do you verify the migration before shipping?</h2>
<p>You verify the migration by running the test suite, hitting every route that exercises tools, watching the DevTools timeline on a streaming chat, and replaying a representative production transcript against the new agent in a staging environment. If any of those four steps surfaces a regression, do not ship.</p>
<p>For the unit-test side, I added one test per agent that walks every expected part type.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// __tests__/weather-agent.test.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> describe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> it</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> expect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">vitest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/agents/weather-agent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">describe</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">weatherAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">  it</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">emits text, tool-weather, and finish parts in order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> weatherAgent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">generate</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">What is the weather in San Francisco?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> partTypes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">messages</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">flatMap</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> m</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">parts</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">type</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    expect</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">partTypes</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toContain</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    expect</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">partTypes</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toContain</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tool-weather</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>For the DevTools verification, wrap the model in middleware and launch the viewer locally.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> wrapLanguageModel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> devToolsMiddleware</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@ai-sdk/devtools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> tracedModel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> wrapLanguageModel</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> gateway</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">openai/gpt-5.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  middleware</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> devToolsMiddleware</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @ai-sdk/devtools</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># http://localhost:4983</span></span></code></pre></figure>
<p>The DevTools viewer renders the full agent timeline: prompt, tool calls, tool results, model responses, and step boundaries. Use it once per migrated route. If a tool call shows up but the matching <code>tool-*</code> render case never fires in your UI, the parts switch is missing a case.</p>
<p>For staging replay, capture three representative production transcripts in your logs, run them through the new agent, and diff the response shape. The token usage breakdown is the cleanest signal because the v6 <code>usage.inputTokenDetails</code> and <code>usage.outputTokenDetails</code> are richer than v5.</p>
<p>If you write your own tools, this is the moment to add <code>inputExamples</code> and <code>strict: true</code> everywhere. They are cheap on the wire and they catch the dumbest tool-call regressions before they reach prod.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">tool</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Search blog posts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  inputSchema</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">string</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">min</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  strict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  inputExamples</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">virtual threads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mcp protocol</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  ]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">  execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> searchBlogPosts</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(query)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>For deeper background on AI SDK patterns that the v6 Agent abstraction now formalizes, see the <a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway deep dive</a> and the <a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Claude Opus 4.7 release and migration guide</a>.</p>
<p>For the original sources, see the <a href="https://vercel.com/blog/ai-sdk-6">AI SDK 6 announcement</a>, the <a href="https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0">official migration guide</a>, the <a href="https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent">ToolLoopAgent reference</a>, and the <a href="https://www.digitalapplied.com/blog/vercel-ai-sdk-v5-to-v6-migration-playbook-2026">Vercel AI SDK v5 to v6 migration playbook</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway Deep Dive</a>. The provider routing layer that the v6 agent string format relies on, with cost and observability patterns.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Claude Opus 4.7 Release and Migration Guide</a>. Provider-side context for the Anthropic models you plug into ToolLoopAgent.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-ai-2-mcp-annotations-tutorial">Spring AI 2.0 MCP Annotations Tutorial</a>. The same tool-loop pattern from the JVM side, and how the v6 MCP client connects both ends.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/replacing-useeffect-data-fetching-server-actions">Replacing useEffect with Server Actions</a>. The Next.js data-fetching shift that pairs with the v6 useChat parts model in App Router chat UIs.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Build a Self-Hosted Slack AI Bot with any CLI Agent]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent</guid>
      <pubDate>Thu, 11 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[How to build a self-hosted Slack AI bot driven by any CLI agent: Socket Mode app, Python glue, token streaming, Approve and Deny buttons, session resume across Codex and Claude Code.]]></description>
      <content:encoded><![CDATA[<p>A Slack bot that DMs you back with the same agent you run from your terminal sounds like a weekend hack, but it is also the cheapest way to give yourself a coding agent that works from anywhere with Slack installed. No browser tab, no separate app, no token billing dashboard to check. You write <code>deploy the staging branch</code> in a Slack DM, the bot streams the plan back into the same message, and asks for permission before it runs <code>kubectl apply</code>.</p>
<p>This guide walks through building exactly that. The shape is a thin Python glue process that uses Slack Socket Mode to receive messages, spawns a CLI agent as a subprocess per turn, and edits the Slack reply in place as tokens stream out. The running example is OpenAI Codex CLI because that is what I shipped first, but the same architecture works with Anthropic Claude Code, Cursor's CLI, or any other agent that exposes a stdio interface. The driver is the only part that changes when you swap agents; the Slack glue, the approval flow, and any MCP tools you add stay identical.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent.webp" alt="Architecture diagram for a self-hosted Slack AI bot. A Slack user posts a message which travels over an outbound Socket Mode WebSocket to a small Python glue process. The glue spawns a CLI agent (Codex in this example) as a subprocess, which makes an HTTPS request to OpenAI and streams tokens back. The glue edits the Slack message in place as tokens arrive." width="1600" height="900"></p>
<p>The walkthrough is structured around three layered variants you can build in order: the basic auto-approve bot, the Slack-button approval gate, and the session-resume version that survives restarts. After that, a short section covers how to swap the Codex driver for Claude Code, Cursor, or anything else without touching the rest of the stack. You can stop at any layer. For most personal bots, basic plus approval is the sweet spot.</p>
<h2 id="why-build-your-own-slack-ai-bot-instead-of-using-the-official-chatgpt-or-claude-integration">Why build your own Slack AI bot instead of using the official ChatGPT or Claude integration?</h2>
<p>The official ChatGPT in Slack and the official Claude in Slack are great for chat. Neither can run code, edit files, or call your internal tools by default. A self-hosted bot wired to a CLI agent like Codex or Claude Code does all three out of the box, because both CLIs ship with <code>shell</code>, file-edit, and an MCP client that picks up any custom tools you register.</p>
<p>The control trade-off is also real. With a self-hosted bot you own the workspace directory the agent reads and writes, the approval rules for destructive commands, the model selection, and the conversation rollouts on disk. If your team has a private codebase or an internal API the bot needs to touch, none of that has to leave your host. The only network call in the whole pipeline is the outbound HTTPS from the agent CLI to its provider, and that traffic is the same as any other agent session you run from a laptop.</p>
<p>Cost is the third reason. ChatGPT Plus at twenty dollars a month includes a generous weekly Codex usage budget, and Claude Pro covers a similar quota on the Anthropic side. OpenAI moved Codex billing onto API-token alignment for paid plans in April 2026, and Claude Code respects your Pro plan in the same way. A personal bot that handles a few dozen short threads a week rarely brushes either cap. If you do exceed the plan limit you can switch to an API key for either provider and pay per token directly, with no other code changes.</p>
<h2 id="how-does-the-slack-and-cli-agent-pipeline-actually-work">How does the Slack and CLI agent pipeline actually work?</h2>
<p>The pipeline has exactly one outbound network call at runtime, and Slack never reaches back into your network. Everything else is local pipes between a Python glue process and the agent subprocess.</p>
<p>The sequence for a single message in a thread looks like this:</p>
<ol>
<li>You type into Slack. The Slack edge serializes the event and pushes it over an outbound WebSocket that the bot opened on startup. That socket is the Socket Mode transport, and it is the only reason no public URL or inbound port is needed.</li>
<li>The Python glue process catches the event in an async handler. It posts a placeholder message in the same thread (<code>thinking…</code>) and remembers the channel and message timestamp.</li>
<li>The glue spawns the agent CLI as an async subprocess. For Codex that is <code>codex exec --json --model gpt-5.5 --cd &#x3C;workspace> &#x3C;prompt></code>; for Claude Code it is <code>claude -p "&#x3C;prompt>" --output-format stream-json --verbose --include-partial-messages --add-dir &#x3C;workspace></code>. In both cases standard output is a JSON Lines stream of events.</li>
<li>The agent CLI itself makes the single HTTPS call out to its provider (OpenAI for Codex, Anthropic for Claude Code). The token stream comes back over that HTTPS connection.</li>
<li>The glue reads stdout line by line, parses each JSON event, accumulates text deltas, and once per second calls <code>chat.update</code> on the placeholder Slack message with the latest accumulated text. The reply edits in place rather than spamming new messages.</li>
<li>When the agent exits, the glue does one final <code>chat.update</code> with the complete reply, then releases the per-thread lock so the next message in the same thread can run.</li>
</ol>
<p>The event family depends on which agent CLI you run. Codex 2026 emits <code>thread.started</code> (carries the session identifier), <code>turn.started</code> and <code>turn.completed</code> (bracket each agent turn), the <code>item.*</code> family (covers agent messages, reasoning, command executions, file changes, MCP tool calls, plan updates), and <code>error</code>. Claude Code with <code>--output-format stream-json --verbose --include-partial-messages</code> emits <code>system/init</code> (session metadata, including <code>session_id</code>), <code>stream_event</code> lines with a nested <code>event.delta</code> payload (where <code>delta.type == 'text_delta'</code> carries token deltas), tool-use events, and <code>system/api_retry</code> when a call retries. Cursor's <code>cursor-agent</code> produces a similar shape under <code>--output-format stream-json --stream-partial-output</code>. The driver layer hides the difference from the rest of the bot.</p>
<h2 id="how-do-you-set-up-the-slack-app-from-a-manifest">How do you set up the Slack app from a manifest?</h2>
<p>The fastest path is the manifest. You paste a single YAML block into Slack's app builder and it provisions the bot user, scopes, event subscriptions, and Socket Mode in one shot. The alternative is twenty clicks across six settings pages.</p>
<p>Open <code>api.slack.com/apps</code>, click <strong>Create New App</strong>, pick <strong>From an app manifest</strong>, choose your workspace, and paste this YAML:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">display_information</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> My AI Bot</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> CLI agent in Slack</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  background_color</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#1d1d1d</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">features</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  bot_user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    display_name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> My AI Bot</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    always_online</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  app_home</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    home_tab_enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    messages_tab_enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    messages_tab_read_only_enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">oauth_config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  scopes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    bot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> app_mentions:read</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> channels:history</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> chat:write</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> im:history</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> im:read</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> im:write</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> users:read</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">settings</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  event_subscriptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    bot_events</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> app_mention</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> message.im</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  interactivity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    is_enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  socket_mode_enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span></code></pre></figure>
<p>After the app is created, you need two tokens. Go to <strong>Settings → Basic Information → App-Level Tokens</strong>, click <strong>Generate Token and Scopes</strong>, add the <code>connections:write</code> scope, and save the token. It starts with <code>xapp-</code> and is what Socket Mode uses to open the WebSocket. Then go to <strong>Settings → Install App</strong>, install to your workspace, and copy the <strong>Bot User OAuth Token</strong> at the top of the page. It starts with <code>xoxb-</code> and is what every <code>chat.postMessage</code> and <code>chat.update</code> call authenticates with.</p>
<p>DMs to the bot work as soon as the install completes. To use <code>@mentions</code> in a channel, run <code>/invite @My AI Bot</code> in that channel from your own account. Otherwise Slack silently drops the event because the bot is not a member.</p>
<h2 id="how-do-you-wire-codex-cli-and-run-the-basic-bot">How do you wire Codex CLI and run the basic bot?</h2>
<p>Install Codex once, log it into ChatGPT, and write a fifty-line Python file that drives it from Slack events. The CLI ships through npm and the Python side needs only Slack Bolt and aiohttp. The same shape applies to any other agent CLI; we use Codex because it has the cleanest stateless <code>exec</code> mode for a per-turn spawn.</p>
<p>Install the CLI and authenticate:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> i</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -g</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @openai/codex</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">codex</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> login</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">   # opens a browser tab to ChatGPT, then writes ~/.codex/auth.json</span></span></code></pre></figure>
<p>On a headless machine, <code>codex login</code> prints a device-code URL. Open it on any other device, finish the OAuth flow there, and the headless host picks up the auth tokens automatically. The token file lives at <code>~/.codex/auth.json</code> regardless of how you sign in.</p>
<p>The Python side is two packages and one script. Save this as <code>requirements.txt</code>:</p>
<pre><code>slack-bolt>=1.18
aiohttp>=3.9
</code></pre>
<p>Then the bot itself. The version below is the auto-approve variant: every tool call the agent decides to run executes without asking. Start here so you can validate the Slack-to-agent pipeline end to end before adding the approval gate.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">slackbot.py - Slack &#x3C;-> CLI agent glue (auto-approve, Codex driver).</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> logging</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> re</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> time</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> collections </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defaultdict</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> slack_bolt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">async_app </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> AsyncApp</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> slack_bolt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">adapter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">socket_mode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">async_handler </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> AsyncSocketModeHandler</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">SLACK_BOT_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">environ</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">SLACK_BOT_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">SLACK_APP_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">environ</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">SLACK_APP_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">AGENT_BIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">environ</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">AGENT_BIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">codex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">AGENT_MODEL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">environ</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">AGENT_MODEL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">gpt-5.5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">AGENT_CWD</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">environ</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">AGENT_CWD</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">expanduser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">~/agent-bot-workspace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">makedirs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">AGENT_CWD</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> exist_ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">True</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">EDIT_INTERVAL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1.0</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">          # seconds between chat.update calls</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">MAX_SLACK_LEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 39_000</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        # chat.update hard cap is 40k chars</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">logging</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">basicConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">level</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">logging</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">INFO</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">log </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> logging</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">getLogger</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slackbot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">app </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> AsyncApp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">SLACK_BOT_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">thread_locks</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">Lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> defaultdict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">Lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> agent_stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">    """</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">Codex driver. Yields text chunks as Codex emits them.</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    proc </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create_subprocess_exec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">        AGENT_BIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">exec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> AGENT_MODEL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--cd</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> AGENT_CWD</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">        stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">subprocess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">PIPE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> stderr</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">subprocess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">PIPE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        async</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> for</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> raw </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">in</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            line </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> raw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">utf-8</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> errors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">replace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">strip</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> not</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> line</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                continue</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                ev </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">loads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">line</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            except</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">JSONDecodeError</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                continue</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            kind </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">startswith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> and</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">agent_message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                delta </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    yield</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> delta</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            elif</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">startswith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> and</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> in</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                yield</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_running: </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">].</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">wait</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">returncode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            err </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stderr</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">read</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">errors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">replace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            yield</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_agent exit </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">returncode</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">: </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">err</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">400</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:]</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    finally</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">returncode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> is</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> None</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">terminate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">@</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">app</span><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">app_mention</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">@</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">app</span><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handle_message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bot_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">subtype</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    channel </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    text </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> re</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">sub</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">&#x3C;@</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">[</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">A-Z0-9</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">strip</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> not</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    thread_ts </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">thread_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    async</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> with</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> thread_locks</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">channel</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">thread_ts</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        placeholder </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">chat_postMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">            channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> thread_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">thread_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_thinking…_</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        )</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        msg_ts </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> placeholder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        accumulated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> last_edit </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0.0</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            async</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> for</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chunk </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">in</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> agent_stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                accumulated </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chunk</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">monotonic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> last_edit </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> EDIT_INTERVAL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">chat_update</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">                        channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">msg_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">                        text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">accumulated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">MAX_SLACK_LEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> or</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_…_</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    )</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    last_edit </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">monotonic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">chat_update</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">                channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">msg_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">                text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">accumulated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">MAX_SLACK_LEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> or</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_(empty reply)_</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        except</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> as</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">agent run failed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">chat_update</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">msg_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'_error: </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">e</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">_'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">():</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slackbot starting</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> AsyncSocketModeHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">app</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> SLACK_APP_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">start_async</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> __name__</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">__main__</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span></code></pre></figure>
<p>Notice the rename from <code>CODEX_*</code> to <code>AGENT_*</code> in the env vars. That is the only thing that changes between agents; the rest of <code>handle_message</code>, the Slack lock, and the streaming throttle are all agent-agnostic. The driver function <code>agent_stream</code> is the swap point.</p>
<p>Export the two tokens, source the file, run it. The shell will block on the WebSocket handler. DM the bot from Slack with a quick <code>hi</code> to confirm the pipeline. You should see a <code>thinking…</code> placeholder appear within a second, then the reply stream into that same message instead of posting a wall of new messages.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> SLACK_BOT_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">xoxb-...</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> SLACK_APP_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">xapp-...</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">python3</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -m</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> venv</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .venv</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x26;&#x26;</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> source</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .venv/bin/activate</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">pip</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -r</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> requirements.txt</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">python</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> slackbot.py</span></span></code></pre></figure>
<p>The two design choices worth flagging. The per-thread <code>asyncio.Lock</code> serializes messages in the same Slack thread so two quick replies cannot clobber each other's <code>chat.update</code> calls. The one-second edit throttle stays under Slack's per-channel-per-second soft cap on <code>chat.update</code>. Crank <code>EDIT_INTERVAL</code> up to two if you see <code>ratelimited</code> errors in the logs.</p>
<h2 id="how-do-you-gate-destructive-tool-calls-with-slack-approve-and-deny-buttons">How do you gate destructive tool calls with Slack Approve and Deny buttons?</h2>
<p>Codex with <code>--ask-for-approval on-request</code> pauses on every tool call that could touch the filesystem or run shell. The bot prints the pending action as a Slack message with Approve and Deny buttons, waits for a click, then writes the decision back into Codex stdin. The pause is a real pause: the agent blocks until the answer arrives, so the user is the rate limit, not the network. Claude Code exposes the same shape through its <code>permission_mode</code> flag, with <code>tool_use_request</code> events on stdout in place of Codex's approval events.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent-approval.webp" alt="Approval gate sequence diagram. The agent CLI emits an approval request event on stdout. The Python glue posts a Slack block with the pending command and Approve and Deny buttons. The user clicks Approve, the glue resolves an asyncio.Future, then writes the approval decision JSON into the agent stdin. The agent resumes and executes the tool." width="1600" height="900"></p>
<p>The wire-level changes are small. Spawn the agent with the approval flag plus an open stdin so you can write the decision back. Keep a dictionary of pending approvals keyed by request id, with each entry holding an <code>asyncio.Future</code>. The Slack button click resolves the future, and the spawn loop writes the response JSON to the agent.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> uuid</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> dict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tuple</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">Future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> agent_stream_with_approval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> thread_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    proc </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create_subprocess_exec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">        AGENT_BIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">exec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--ask-for-approval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">on-request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> AGENT_MODEL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--cd</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> AGENT_CWD</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">        stdin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">subprocess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">PIPE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">        stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">subprocess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">PIPE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">        stderr</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">subprocess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">PIPE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handle_approval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        approval_id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">uuid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">uuid4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        title </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        details </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">patch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        fut </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get_running_loop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create_future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">fut</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> thread_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">chat_postMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">            channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">channel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> thread_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">thread_ts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">            text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'Agent wants to run: </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">title</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">            blocks</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">section</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mrkdwn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                 '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'*Agent wants to run `</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">title</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">`*'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">section</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mrkdwn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                 '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'```</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">dumps</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">details</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> indent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)[:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2800</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">```'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">actions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">block_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'approve:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">{</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">approval_id</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                 '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">elements</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">button</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">style</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">primary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">action_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">approve</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                     '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">plain_text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Approve</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                     '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">button</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">style</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">danger</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">action_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">deny</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                     '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">plain_text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Deny</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                     '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                 ]},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            ],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            decision </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">wait_for</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">fut</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> timeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">600</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        except</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">TimeoutError</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            decision </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">deny</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        finally</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">pop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> None</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">approval_response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">decision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> decision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stdin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">write</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">((</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">dumps</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">encode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stdin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">drain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    async</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> for</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> raw </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">in</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        line </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> raw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">utf-8</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> errors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">replace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">strip</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> not</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> line</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            continue</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            ev </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">loads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">line</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        except</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">JSONDecodeError</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            continue</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        kind </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">approval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> in</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> kind </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> kind </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tool_use_request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create_task</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">handle_approval</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            continue</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> kind</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">startswith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> and</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">agent_message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            delta </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> or</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                yield</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> delta</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">wait</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">@</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">app</span><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">action</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">approve</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> on_approve</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">ack</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> ack</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    approval_id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">actions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> approval_id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">in</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pending </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">and</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> not</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">].</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">done</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">():</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">].</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">set_result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">approve</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">@</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">app</span><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">action</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">deny</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> on_deny</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">ack</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> ack</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    approval_id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">actions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> approval_id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">in</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pending </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">and</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> not</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">].</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">done</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">():</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">approval_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">][</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">].</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">set_result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">deny</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>Three things to know before you ship this in production. First, the precise approval JSON event names drift between agent versions, so dump one to your logs with <code>codex exec --json --ask-for-approval on-request "echo hi"</code> (or the Claude Code equivalent) and adjust the kind check if your build emits a different shape. Second, the <code>pending</code> dictionary lives in process memory. If the bot crashes while a button is unanswered, the click will silently no-op because the matching <code>Future</code> is gone. Persisting the table to sqlite is the durable fix, but for personal use, just re-send the original message after a restart. Third, the 600-second timeout auto-denies anything you forget to click, so destructive runs do not hang the agent forever.</p>
<p>The companion post on <a href="https://www.rabinarayanpatra.com/blogs/how-to-add-mcp-tools-slack-ai-bot">adding custom MCP tools</a> builds on this approval flow and shows how to gate any tool you write yourself behind the same Approve and Deny block. If you only ever expose read-only MCP tools, you can skip approval entirely and run the basic variant.</p>
<h2 id="how-does-the-bot-remember-context-across-messages-and-restarts">How does the bot remember context across messages and restarts?</h2>
<p>The basic variant rebuilds context every turn by re-fetching the Slack thread history and stuffing it into the prompt. That works, but it costs input tokens on every turn and forgets the agent's internal tool memory between turns. The stateful variant fixes both by storing an agent session identifier per Slack thread and resuming the same session on every new message.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-build-self-hosted-slack-ai-bot-cli-agent-stateful.webp" alt="Stateful resume timeline diagram showing four messages in the same Slack thread. After the first message, the glue captures the session_id from the agent&#x27;s first event and writes it into sqlite. Subsequent messages spawn the agent with a resume flag, so the agent retains tool state across turns and the bot survives a restart between messages without losing the conversation." width="1600" height="900"></p>
<p>Codex 2026 exposes resume as a subcommand: <code>codex exec resume &#x3C;session_id></code> continues a specific session, and <code>codex exec resume --last</code> continues the most recent one in the current workspace. The session identifier shows up in the <code>thread.started</code> event at the start of each run, and Codex writes the full rollout to <code>~/.codex/sessions/YYYY/MM/DD/rollout-&#x3C;timestamp>-&#x3C;uuid>.jsonl</code> on disk. Claude Code stores its sessions under <code>~/.claude/sessions/</code> and resumes via a <code>--resume &#x3C;session_id></code> flag on the same subcommand. The mapping from a Slack thread to an agent session is what you store on your side.</p>
<p>A tiny sqlite schema is enough:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> TABLE</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IF</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> EXISTS</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> thread_sessions (</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    slack_channel    </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">TEXT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    slack_thread_ts  </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">TEXT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    agent_session_id </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">TEXT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    created_at       </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">REAL</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    last_used_at     </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">REAL</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    PRIMARY KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (slack_channel, slack_thread_ts)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>The driver becomes a few lines longer: look up the session before spawning, prefer <code>&#x3C;agent> exec resume &#x3C;id></code> when a row exists, capture the new identifier from the first session event of every run, and write it back on completion. Fall back to a fresh spawn if resume errors out, which is what happens when the rollout file has been deleted or the flag is not supported on an older CLI build. With sqlite on disk and the rollouts on disk, the bot survives restarts and reboots without losing thread context.</p>
<p>A periodic garbage-collection task drops rows older than thirty days and matches a <code>find ~/.&#x3C;agent>/sessions -name '*.jsonl' -mtime +30 -delete</code> cleanup you can run from cron. For most personal setups, disk usage is negligible and you can skip the GC entirely. If you only have one-shot threads with no follow-up, skip the stateful variant entirely; the per-turn re-prompt is cheap and stateless makes a stronger isolation guarantee.</p>
<h2 id="how-do-you-swap-codex-for-claude-code-cursor-or-another-cli-agent">How do you swap Codex for Claude Code, Cursor, or another CLI agent?</h2>
<p>Rewrite the <code>agent_stream</code> function and update three environment variables. Everything else stays put. The Slack glue, the per-thread lock, the approval button handlers, the sqlite session table, and any MCP tools you have already configured all keep working without a single change.</p>
<p>The driver swap is mostly mechanical: each agent CLI has its own flag names, its own JSON event family, and its own session-resume convention. The table below shows the surfaces I have driven so far:</p>
<table>
<thead>
<tr>
<th>Surface</th>
<th>OpenAI Codex CLI</th>
<th>Anthropic Claude Code</th>
<th>Cursor (<code>cursor-agent</code>)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Headless invocation</td>
<td><code>codex exec "&#x3C;prompt>"</code></td>
<td><code>claude -p "&#x3C;prompt>"</code></td>
<td><code>cursor-agent -p "&#x3C;prompt>"</code></td>
</tr>
<tr>
<td>Streaming output</td>
<td><code>--json</code></td>
<td><code>--output-format stream-json --verbose --include-partial-messages</code></td>
<td><code>--output-format stream-json --stream-partial-output</code></td>
</tr>
<tr>
<td>Model flag</td>
<td><code>--model gpt-5.5</code></td>
<td><code>--model claude-sonnet-4-6</code> (or <code>sonnet</code>/<code>opus</code> aliases)</td>
<td><code>--model &#x3C;name></code></td>
</tr>
<tr>
<td>Workspace flag</td>
<td><code>--cd &#x3C;dir></code></td>
<td><code>--add-dir &#x3C;dir></code> (multiple allowed)</td>
<td><code>--workspace &#x3C;path></code></td>
</tr>
<tr>
<td>Approval flag</td>
<td><code>--ask-for-approval on-request</code></td>
<td><code>--permission-mode acceptEdits</code> + <code>--allowedTools "Bash,Read,Edit"</code></td>
<td><code>--approve-mcps</code> and interactive prompts</td>
</tr>
<tr>
<td>Text-delta event</td>
<td><code>item.*</code> with <code>agent_message</code> item</td>
<td><code>stream_event</code> with <code>event.delta.type == "text_delta"</code></td>
<td><code>stream_event</code> with delta payload</td>
</tr>
<tr>
<td>Session id surface</td>
<td><code>thread.started.thread_id</code></td>
<td><code>system/init</code> event (<code>session_id</code>) and top-level <code>session_id</code> in JSON</td>
<td><code>system/init</code> event (<code>session_id</code>)</td>
</tr>
<tr>
<td>Resume</td>
<td><code>codex exec resume &#x3C;id></code> / <code>--last</code></td>
<td><code>claude -p --resume &#x3C;id></code> / <code>--continue</code></td>
<td><code>cursor-agent -p --resume &#x3C;id></code> / <code>--continue</code></td>
</tr>
<tr>
<td>Auth storage</td>
<td><code>~/.codex/auth.json</code></td>
<td>OAuth / keychain, or <code>ANTHROPIC_API_KEY</code> env</td>
<td>Cursor account login</td>
</tr>
<tr>
<td>MCP config</td>
<td><code>~/.codex/config.toml</code> (<code>[mcp_servers.&#x3C;name>]</code>)</td>
<td><code>.mcp.json</code> in project root or <code>--mcp-config &#x3C;file></code></td>
<td><code>.cursor/mcp.json</code> and <code>cursor-agent mcp</code> subcommands</td>
</tr>
</tbody>
</table>
<p>A Claude Code driver function looks roughly like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> agent_stream_claude</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit">prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">):</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">    """</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">Claude Code driver (`claude -p` with stream-json).</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#032F62;--shiki-light-font-style:inherit">"""</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    proc </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create_subprocess_exec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">        AGENT_BIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">-p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--output-format</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">stream-json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--verbose</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--include-partial-messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> AGENT_MODEL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">--add-dir</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> AGENT_CWD</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">        stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">subprocess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">PIPE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">        stderr</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">asyncio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">subprocess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#005CC5">PIPE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    async</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> for</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> raw </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">in</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">stdout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        line </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> raw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">decode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">utf-8</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> errors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">replace</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">strip</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> not</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> line</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            continue</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            ev </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">loads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">line</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        except</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">JSONDecodeError</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            continue</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">stream_event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            delta </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ev</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {})</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text_delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                yield</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> delta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> proc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">wait</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span></code></pre></figure>
<p>The shape is identical to the Codex driver: spawn, read JSONL, dispatch on <code>type</code>, yield text deltas. Only the field names move. Switching the bot to Claude Code in production is a config change plus a function pointer in <code>handle_message</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">DRIVERS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">    '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">codex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> agent_stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">    '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> agent_stream_claude</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">agent_stream_fn </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> DRIVERS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">os</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">environ</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">AGENT_KIND</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">codex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)]</span></span></code></pre></figure>
<p>Two design notes that matter when you actually do this. The Claude Code CLI flag set for headless streaming is <code>-p</code> (or <code>--print</code>) plus <code>--output-format stream-json --verbose --include-partial-messages</code>; without all three you do not get token deltas. The other note: each agent has its own MCP config (Codex uses <code>~/.codex/config.toml</code>, Claude Code reads <code>.mcp.json</code> or a file via <code>--mcp-config</code>, Cursor uses <code>.cursor/mcp.json</code>), but the same custom MCP server can be registered in all of them. Your custom <code>query_orders</code> tool works under any agent without code changes.</p>
<h2 id="how-do-you-run-the-bot-as-a-service-on-linux-or-mac">How do you run the bot as a service on Linux or Mac?</h2>
<p>On Linux it is a fifteen-line systemd unit. On Mac it is a launchd plist plus <code>caffeinate -i</code> to stop the laptop sleeping while the bot runs. Either way, the goal is the same: the process restarts on crash, comes back after a reboot, and writes logs to a place you can tail.</p>
<p>The systemd unit at <code>/etc/systemd/system/slackbot.service</code> looks like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ini" data-theme="material-theme github-light"><code data-language="ini" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[Unit]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Slack &#x3C;-> CLI agent bot</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">After</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">network-online.target</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[Service]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">simple</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">slackbot</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">WorkingDirectory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/opt/slackbot</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Environment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">HOME</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/opt/slackbot</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Environment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">PATH</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/usr/local/bin:/usr/bin:/bin</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">EnvironmentFile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/opt/slackbot/.env</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">ExecStart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/opt/slackbot/.venv/bin/python /opt/slackbot/slackbot.py</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Restart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">always</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">RestartSec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">5</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[Install]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">WantedBy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">multi-user.target</span></span></code></pre></figure>
<p>Three quirks bite first-time systemd users. <code>EnvironmentFile=</code> does not expand <code>~</code> or run <code>export</code>, so the <code>.env</code> file must be plain <code>KEY=value</code> lines with absolute paths. The systemd <code>PATH</code> is minimal, so either set <code>AGENT_BIN=/usr/local/bin/codex</code> (or the Claude Code path) in <code>.env</code> or add the <code>Environment=PATH=...</code> line shown above. And <code>HOME=/opt/slackbot</code> is what tells the agent CLI where to find its auth file, which is the file you populated when you ran <code>codex login</code> or <code>claude login</code> as the <code>slackbot</code> user during setup.</p>
<p>On Mac the equivalent is a launchd plist at <code>~/Library/LaunchAgents/com.you.slackbot.plist</code> that wraps the Python invocation in <code>caffeinate -i</code> so the laptop never sleeps while the bot is running. <code>KeepAlive=true</code> restarts the process on crash, and <code>RunAtLoad=true</code> starts it on login.</p>
<p>The deploy from a clean Hetzner or DigitalOcean box is short:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> adduser</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --system</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --group</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --home</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /opt/slackbot</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> slackbot</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> apt</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -y</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> python3-venv</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> nodejs</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> sqlite3</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> i</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -g</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @openai/codex</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">     # or: install Claude Code per Anthropic docs</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># rsync slackbot.py + requirements.txt up, then:</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> slackbot</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> bash</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -c</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">cd /opt/slackbot &#x26;&#x26; python3 -m venv .venv &#x26;&#x26; .venv/bin/pip install -r requirements.txt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> slackbot</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -H</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> bash</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -c</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">codex login</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">       # device-code flow on another browser</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> daemon-reload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x26;&#x26;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> enable</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --now</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> slackbot</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> journalctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> slackbot</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span></span></code></pre></figure>
<p><code>ufw deny incoming, allow outgoing</code> is enough for the firewall side. Socket Mode is outbound only, so no port has to be opened to the public internet.</p>
<h2 id="what-breaks-in-production-and-how-do-you-fix-it">What breaks in production and how do you fix it?</h2>
<p>Most failures show up in the first hour and are mechanical. The high-value diagnostic commands are short.</p>
<p><code>codex exec --json "ping" | head -20</code> (or <code>claude --json</code> with a piped prompt) confirms the agent CLI itself works independent of Slack, which is the single most useful split test when the bot stays quiet. <code>curl -s https://slack.com/api/auth.test -H "Authorization: Bearer $SLACK_BOT_TOKEN" | jq</code> confirms the bot token is live and points at the right workspace. <code>sudo journalctl -u slackbot -f</code> on Linux or <code>tail -f /tmp/slackbot.err.log</code> on Mac shows the parser output and the agent exit code if something dies mid-stream.</p>
<p>The failures I have hit, in order of how often they bite. Swapped <code>xoxb-</code> and <code>xapp-</code> tokens cause <code>slack_bolt.error.BoltError: invalid_auth</code> on startup. <code>chat.update</code> rate-limited errors mean lower the per-second edit cadence with a bigger <code>EDIT_INTERVAL</code>. <code>agent: command not found</code> under systemd means absolute path or <code>Environment=PATH=...</code> in the unit. <code>codex login</code> not completing on a headless VM means open the device-code URL on your laptop instead. Approval buttons that do nothing on click usually mean Interactivity is not enabled in the Slack app settings, which is the one manifest detail that drifted between platform versions. The bot replying once then going silent is the agent plan's weekly cap; switch to an API key for the remainder of the week or upgrade the plan.</p>
<p>If you want to extend the bot beyond what the built-in agent tools cover, the next step is custom MCP servers. Both Codex and Claude Code act as MCP clients by default and read server definitions from their own config files, so anything you can write as a Python function with a docstring becomes a tool the bot can call regardless of which agent you have selected. The companion guide on <a href="https://www.rabinarayanpatra.com/blogs/how-to-add-mcp-tools-slack-ai-bot">adding custom MCP tools to your Slack Codex bot</a> walks through the FastMCP setup, the wire format, and the production patterns I keep going back to.</p>
<p>For more on the moving pieces, see the <a href="https://docs.slack.dev/tools/bolt-python/concepts/socket-mode">Slack Bolt Python Socket Mode docs</a>, the <a href="https://developers.openai.com/codex/noninteractive">Codex CLI non-interactive mode reference</a>, and the <a href="https://developers.openai.com/codex/agent-approvals-security">Codex agent approvals and security guide</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-to-add-mcp-tools-slack-ai-bot">How to Add Custom MCP Tools to Your Slack Codex Bot</a>. The companion post that extends this bot with custom tools your team's APIs and databases expose to the agent.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/build-stateless-mcp-server-2026-07-28-spec">How to Build a Stateless MCP Server for the 2026-07-28 Spec</a>. The protocol the Slack bot speaks to any MCP tool it loads, explained from the server side.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-ai-2-mcp-annotations-tutorial">Spring AI 2.0 MCP Annotations: From Tool to Production</a>. A Java perspective on the same MCP protocol the bot uses, useful if your stack is JVM-heavy.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[10 Best Claude Skills for Developers in 2026]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/best-claude-skills-for-developers-2026</link>
      <guid>https://www.rabinarayanpatra.com/blogs/best-claude-skills-for-developers-2026</guid>
      <pubDate>Tue, 09 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[10 best Claude skills for developers in 2026. Real picks I use every week, ranked by impact and GitHub stars, with direct install links for each.]]></description>
      <content:encoded><![CDATA[<p>Anthropic shipped Skills in October 2025. A year and a half later, the skill ecosystem is real. <a href="https://github.com/anthropics/skills">anthropics/skills</a> has 141,849 stars. <a href="https://github.com/obra/superpowers">obra/superpowers</a> has 209,283 stars. The <a href="https://agentskills.io">open standard</a> is live.</p>
<p>I install a lot of skills. Most do not earn the slot. These ten do. Quick-look format below: name, direct link, what it does, what I use it for. Ranked by impact in real work, with parent-repo star count as a popularity tiebreaker.</p>
<h2 id="what-are-claude-skills">What are Claude Skills?</h2>
<p>A Claude Skill is a folder with a <code>SKILL.md</code> file that teaches Claude one specific task. When the model sees a task that matches, it loads the skill and follows the playbook inside. The point is making behavior reliable. You write the convention once; every future session follows it.</p>
<p>Skills are generally available across claude.ai, Claude Code, and the Claude API (the API still requires the <code>skills-2025-10-02</code> beta header).</p>
<h2 id="how-do-you-install-claude-skills">How do you install Claude Skills?</h2>
<p>Three install paths cover everything:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Path 1: Anthropic official skills (clone into your skills folder)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">mkdir</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -p</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/.claude/skills</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">cd</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/.claude/skills</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> clone</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --depth</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://github.com/anthropics/skills.git</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> anthropic-skills</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Path 2: superpowers plugin (run inside a Claude Code session)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">/plugin</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> superpowers@claude-plugins-official</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Path 3: per-project skills (drop into .claude/skills/ in the repo root)</span></span></code></pre></figure>
<p>After install, restart your Claude Code session once.</p>
<h2 id="what-are-the-10-best-claude-skills-for-developers">What are the 10 best Claude skills for developers?</h2>
<p>Ranked by impact, parent-repo stars listed for transparency.</p>
<h3 id="1-brainstorming">1. brainstorming</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/obra/superpowers/tree/main/skills/brainstorming">obra/superpowers · brainstorming</a> (parent repo 209k stars)</li>
<li><strong>What it does:</strong> before any feature work, runs a structured exploration of intent, requirements, and design. Writes a short design memo. Asks for approval before any code.</li>
<li><strong>Use it for:</strong> every "build X" or "add Y" request. Catches the one design question you forgot to ask.</li>
</ul>
<h3 id="2-systematic-debugging">2. systematic-debugging</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/obra/superpowers/tree/main/skills/systematic-debugging">obra/superpowers · systematic-debugging</a> (209k stars)</li>
<li><strong>What it does:</strong> forces a four-step bug protocol: reproduce deterministically, hypothesize root cause, prove or disprove with the smallest test, fix only the root cause.</li>
<li><strong>Use it for:</strong> every flaky test and every "why is this slow?" investigation. Replaces try-catch reflex with real root-cause work.</li>
</ul>
<h3 id="3-test-driven-development">3. test-driven-development</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/obra/superpowers/tree/main/skills/test-driven-development">obra/superpowers · test-driven-development</a> (209k stars)</li>
<li><strong>What it does:</strong> writes the failing test first, runs it, shows the failure, then writes the implementation. Re-runs after each pass.</li>
<li><strong>Use it for:</strong> any new function or feature where correctness matters. Stops Claude from "verifying" by re-reading its own code.</li>
</ul>
<h3 id="4-writing-plans">4. writing-plans</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/obra/superpowers/tree/main/skills/writing-plans">obra/superpowers · writing-plans</a> (209k stars)</li>
<li><strong>What it does:</strong> turns a vague request into a numbered plan with critical files identified, tradeoffs called out, and explicit checkpoints.</li>
<li><strong>Use it for:</strong> any task that touches 3+ files or crosses a boundary (API, DB, infra). Prevents "looks done but breaks in three places."</li>
</ul>
<h3 id="5-executing-plans">5. executing-plans</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/obra/superpowers/tree/main/skills/executing-plans">obra/superpowers · executing-plans</a> (209k stars)</li>
<li><strong>What it does:</strong> runs a plan one step at a time in a separate session, with explicit checkpoints between steps.</li>
<li><strong>Use it for:</strong> following up on a writing-plans output. Lets you resume mid-plan after a fix or a context switch.</li>
</ul>
<h3 id="6-subagent-driven-development">6. subagent-driven-development</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/obra/superpowers/tree/main/skills/subagent-driven-development">obra/superpowers · subagent-driven-development</a> (209k stars)</li>
<li><strong>What it does:</strong> spawns a subagent per independent subtask, runs them in parallel, merges results. Enforces clean handoff contracts.</li>
<li><strong>Use it for:</strong> any task with parallelizable pieces (audit v1 vs v2 API, scan 5 services, compare 3 SDKs). Saves wall time and context budget.</li>
</ul>
<h3 id="7-verification-before-completion">7. verification-before-completion</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/obra/superpowers/tree/main/skills/verification-before-completion">obra/superpowers · verification-before-completion</a> (209k stars)</li>
<li><strong>What it does:</strong> runs the actual app or test suite end-to-end before reporting the task done. Surfaces evidence of what worked and what broke.</li>
<li><strong>Use it for:</strong> the "tests pass but the feature is broken" class of bug. Pair with brainstorming + TDD for the full discipline loop.</li>
</ul>
<h3 id="8-skill-creator">8. skill-creator</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/anthropics/skills/tree/main/skills/skill-creator">anthropics/skills · skill-creator</a> (parent repo 142k stars)</li>
<li><strong>What it does:</strong> interviews you on trigger language, steps, and references, then writes a new <code>SKILL.md</code> in the right shape.</li>
<li><strong>Use it for:</strong> every repeatable task you do more than twice a week. The meta-skill that pays for itself the first time.</li>
</ul>
<h3 id="9-graphify">9. graphify</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/safishamsi/graphify">safishamsi/graphify</a> (55k stars)</li>
<li><strong>What it does:</strong> runs an AST pass over your project, identifies god nodes, maps community structure, writes a queryable wiki under <code>graphify-out/</code>.</li>
<li><strong>Use it for:</strong> any repo over 30k lines. Future sessions read the wiki instead of grep-walking the file tree.</li>
</ul>
<h3 id="10-mcp-builder">10. mcp-builder</h3>
<ul>
<li><strong>Skill:</strong> <a href="https://github.com/anthropics/skills/tree/main/skills/mcp-builder">anthropics/skills · mcp-builder</a> (parent repo 142k stars)</li>
<li><strong>What it does:</strong> scaffolds an MCP server end-to-end (transport choice, tool annotations, auth, deployment hints). Generates a working Spring/Node/Python skeleton.</li>
<li><strong>Use it for:</strong> the first MCP server you build. Replaces the "read 4 docs to get started" ritual with one prompt.</li>
</ul>
<h2 id="which-claude-skills-should-you-install-first">Which Claude skills should you install first?</h2>
<p>Install in this order:</p>
<ol>
<li>Run <code>/plugin install superpowers@claude-plugins-official</code> in Claude Code. Pulls 7 of the 10 skills above.</li>
<li>Clone <a href="https://github.com/anthropics/skills">anthropics/skills</a> into <code>~/.claude/skills/anthropic-skills/</code>. Gives you skill-creator, mcp-builder, and a wider base library.</li>
<li>Install <a href="https://github.com/safishamsi/graphify">safishamsi/graphify</a> when your project crosses 30k lines.</li>
<li>Run skill-creator once. Build a custom skill for the most-repeated task in your current project. Writing one teaches you how to read others.</li>
</ol>
<p>The thing nobody says out loud: the compounding only kicks in if you actually use them. Skills are habits, not features. Install one. Use it for two weeks. Then install the next.</p>
<p>For more on Claude Skills internals, see the <a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills">Anthropic engineering deep-dive on agent skills</a>, the <a href="https://code.claude.com/docs/en/skills">Claude Code skills documentation</a>, and the <a href="https://agentskills.io">open skills standard at agentskills.io</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects</a>. Where skills end and MCP servers begin.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-code-routines-ci-automation">Claude Code Routines: Async CI Automation Just Became Real</a>. Use skills inside scheduled CI runs.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Claude Opus 4.7 Release and Migration Guide</a>. The model these skills run on top of.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Set Up Sanity Studio in Next.js 16 (Embedded Dashboard)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-set-up-sanity-studio-nextjs-16</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-set-up-sanity-studio-nextjs-16</guid>
      <pubDate>Thu, 04 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[How to set up Sanity Studio in Next.js 16 step by step: install next-sanity, mount the embedded /studio route, write a real post schema, query from a Server Component, fix CORS, and deploy to Vercel.]]></description>
      <content:encoded><![CDATA[<p>The default mental model for a headless CMS is two places: the CMS dashboard lives over there on the vendor domain, and your site lives over here on Vercel. Editors bounce between tabs, you copy environment variables between systems, and somebody always forgets to allowlist a domain in CORS.</p>
<p>Sanity does not require any of that. The Studio is a React app you ship inside your Next.js project. One catch-all route mounts the editor at <code>/studio</code>, your schemas live in TypeScript next to the rest of your code, and the whole thing deploys on the same Vercel push that ships your blog. This post walks through the full setup for Next.js 16: install, config, a real post + author + category schema, querying from a Server Component, the CORS fix every first-time user hits, and the Vercel deploy.</p>
<p>I built this exact integration into a small content site I run, and I will use that as the running example. If you already have a Next.js 16 app and want a working CMS dashboard inside it by the end of the next hour, you are in the right place.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-sanity-studio-nextjs-16.webp" alt="Sanity Studio embedded in a Next.js 16 app. Left panel shows the /studio route with Post, Author, and Category document types and a Publish button. Right panel shows the /blog route rendering published posts." width="1600" height="900"></p>
<h2 id="what-is-sanity-studio-and-why-embed-it-in-nextjs">What is Sanity Studio and why embed it in Next.js?</h2>
<p>Sanity Studio is the editor UI that ships with Sanity, a hosted content platform that splits cleanly into two pieces: an open-source React app for editors, and a managed document store called the Content Lake. The Studio is the part you customize. The Lake is the part you query.</p>
<p>The reason to embed it in Next.js, instead of running it on the default <code>sanity.studio</code> subdomain, is that almost every project ends up wanting the same three things. Editors should sign in once on the same domain as the site. Schemas should live in TypeScript next to the components that render them. Deploys should be one Vercel push, not two. Embedding gets you all three for the cost of one catch-all route.</p>
<p>The latest stable release is <code>sanity@5.30.0</code>, with the official Next.js toolkit at <code>next-sanity@13.0.11</code> as of June 2026. Both target the App Router and React 19, and Sanity migrated its own docs platform to Next.js 16 earlier this year, so the integration path is well-trodden.</p>
<h2 id="what-do-you-need-before-installing-sanity-in-nextjs-16">What do you need before installing Sanity in Next.js 16?</h2>
<p>You need a working Next.js 16 App Router project on Node 20 or newer, plus a free Sanity account. That is the entire prerequisite list.</p>
<p>Concretely:</p>
<ul>
<li><strong>Node 20+.</strong> The current Studio package drops support for Node 18 and 19. Check with <code>node -v</code>.</li>
<li><strong>A Next.js 16 app on the App Router.</strong> Pages Router works too with a different route file, but this guide assumes App Router because that is what Next.js 16 starters default to.</li>
<li><strong>A free Sanity account at <code>sanity.io</code>.</strong> The free tier gives you a project, a dataset, three editor seats, and 10k API requests per month, which is enough for a personal site.</li>
<li><strong>A package manager.</strong> I use <code>npm</code>, but <code>pnpm</code> and <code>yarn</code> work without changes.</li>
</ul>
<p>If you are starting from scratch, the rest of this guide assumes the same shape as a typical Next.js 16 app: a <code>src/app</code> directory, TypeScript on, Tailwind optional. The same steps apply if you keep <code>app</code> at the repo root.</p>
<h2 id="how-do-you-scaffold-sanity-studio-inside-an-existing-nextjs-app">How do you scaffold Sanity Studio inside an existing Next.js app?</h2>
<p>Run <code>npx sanity@latest init</code> at the root of your Next.js project, then install the Next.js bridge package separately. The init command creates the Sanity project on the server, writes a starter <code>sanity.config.ts</code>, and adds a <code>sanity/</code> directory with example schemas.</p>
<p>The full sequence from a clean Next.js 16 repo:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 1. Provision the project and write starter config</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> sanity@latest</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> init</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 2. Install the Next.js bridge and the image URL helper</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> next-sanity</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @sanity/image-url</span></span></code></pre></figure>
<p>When <code>sanity init</code> runs, it walks you through a short CLI prompt:</p>
<ol>
<li>Sign in with email, GitHub, or Google (browser tab opens).</li>
<li>Pick "Create new project" and give it a name.</li>
<li>Pick <code>production</code> as the default dataset.</li>
<li>Choose "Yes" when it asks to add the example schema (you will replace it shortly).</li>
<li>Pick TypeScript and the <code>npm</code> package manager when prompted.</li>
</ol>
<p>The CLI prints two values at the end: a project ID (an eight-character string) and a dataset name (<code>production</code>). Drop them into <code>.env.local</code> exactly as they are:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># .env.local</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">NEXT_PUBLIC_SANITY_PROJECT_ID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">your_project_id</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">NEXT_PUBLIC_SANITY_DATASET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">production</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SANITY_API_READ_TOKEN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">optional_for_drafts</span></span></code></pre></figure>
<p>The <code>NEXT_PUBLIC_</code> prefix is required. The Studio runs in the browser and needs to read both values from <code>process.env</code> at build time. The third variable is only needed later if you want the public site to render unpublished drafts for previews.</p>
<h2 id="how-do-you-wire-the-embedded-studio-route-at-studio">How do you wire the embedded Studio route at /studio?</h2>
<p>Create two files: <code>sanity.config.ts</code> at the project root, and a catch-all route at <code>app/studio/[[...tool]]/page.tsx</code>. The route imports the config and hands it to the <code>NextStudio</code> component, which does the actual rendering.</p>
<p>The config file is where you declare the project ID, dataset, base path, plugins, and your schema list. A minimal version for a blog looks like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// sanity.config.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sanity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> structureTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sanity/structure</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> visionTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@sanity/vision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> schemaTypes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">./sanity/schemaTypes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> default</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> defineConfig</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">default</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">My Blog Studio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  projectId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> process</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">env</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">NEXT_PUBLIC_SANITY_PROJECT_ID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  dataset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> process</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">env</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">NEXT_PUBLIC_SANITY_DATASET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  basePath</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/studio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  plugins</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">structureTool</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> visionTool</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  schema</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> types</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> schemaTypes </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Two things matter here. <code>basePath: '/studio'</code> tells the Studio where it is mounted, so internal links point at <code>/studio/structure/post</code> and not the root. <code>plugins</code> always includes <code>structureTool()</code> (the document list and editor) and, for development, <code>visionTool()</code> (a GROQ query playground).</p>
<p>The route file is short. It imports the config, exports the <code>metadata</code> and <code>viewport</code> that <code>next-sanity</code> already prepared, and renders the Studio:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// app/studio/[[...tool]]/page.tsx</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> NextStudio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next-sanity/studio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> config </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">../../../sanity.config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> dynamic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">force-static</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> metadata</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> viewport</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next-sanity/studio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> StudioPage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">NextStudio</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} />;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>force-static</code> makes the route itself a static shell. The Studio is a heavy client bundle and there is no benefit to re-rendering the shell on every request. The actual editor work happens client-side once the shell ships.</p>
<p>Once these two files exist, start the dev server with <code>npm run dev</code> and open <code>http://localhost:3000/studio</code>. The Studio loads, asks you to sign in, and then shows the document list. Right now the list is empty because you have not defined any schema types yet.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-sanity-studio-nextjs-16-studio-dashboard.webp" alt="Sanity Studio empty document type picker with Artist, Event, and Venue options. Image: Sanity Inc." width="1600" height="1058">
<em>Source: <a href="https://www.sanity.io/learn/course/day-one-with-sanity-studio">Sanity Learn: Day one with Sanity Studio</a></em></p>
<p>The screenshot above is from the official Sanity learn course, showing the Studio chrome and the document creation menu populated by schema types. Once you add the schemas in the next section, your menu will show Post, Author, and Category in place of those examples.</p>
<p>This three-layer architecture is the part that makes the embedded approach worthwhile. The same browser session talks to two Next.js routes that talk to the same Sanity project, with the editor on the write path and the public pages on the read path:</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-sanity-studio-nextjs-16-architecture.webp" alt="Three-layer architecture diagram. Top: Browser. Middle: Next.js 16 App Router with /studio mounting NextStudio for the write path and /blog using a Server Component with sanityClient.fetch for the read path. Bottom: Sanity Content Lake holding the project ID, dataset, and CORS allowlist." width="1600" height="900"></p>
<h2 id="how-do-you-write-a-real-content-schema-post--author--category">How do you write a real content schema (post + author + category)?</h2>
<p>Create a <code>sanity/schemaTypes/</code> directory with one file per document type, then export them as an array from <code>sanity/schemaTypes/index.ts</code>. Each file describes the shape of one document using the <code>defineType</code> and <code>defineField</code> helpers from the <code>sanity</code> package.</p>
<p>For a blog you want three documents. A <code>post</code> holds the article. An <code>author</code> holds the person who wrote it. A <code>category</code> groups posts. Posts reference authors and categories, so editors fill in those fields with a dropdown instead of free text.</p>
<p>Start with <code>post.ts</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// sanity/schemaTypes/post.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineField</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sanity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> defineType</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">document</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  fields</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">      validation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">required</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">max</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">80</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      options</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> maxLength</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 96</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">      validation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">required</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">reference</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> to</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">] </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">reference</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      to</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mainImage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      options</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> hotspot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      fields</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">alt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Alt text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">publishedAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">datetime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">blockContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  ]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>A few choices worth flagging. <code>validation: r => r.required().max(80)</code> runs in the Studio in real time, so editors see the red warning the moment a title is empty or too long. <code>options: { source: 'title' }</code> on the slug field wires up the "Generate" button, which converts the title to a URL slug. <code>options: { hotspot: true }</code> on the image field lets editors pick a focal point so Sanity can crop responsively without cutting off heads.</p>
<p><code>author.ts</code> and <code>category.ts</code> are smaller:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// sanity/schemaTypes/author.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineField</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sanity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> defineType</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">document</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  fields</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> validation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">required</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">() </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      options</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">      validation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">required</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">avatar</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> options</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> hotspot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> rows</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  ]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// sanity/schemaTypes/category.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineField</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sanity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> defineType</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">document</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  fields</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> validation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">required</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">() </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      options</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">      validation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">required</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    defineField</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> rows</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  ]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>The <code>blockContent</code> type that the post body uses is the portable text format Sanity ships for rich text. It is a one-line export:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// sanity/schemaTypes/blockContent.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> defineType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sanity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> blockContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> defineType</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">blockContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Block Content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">array</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">block</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> },</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">image</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> options</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> hotspot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>The index file wires them together:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// sanity/schemaTypes/index.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">./post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">./author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">./category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> blockContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">./blockContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> schemaTypes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> blockContent]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Reload <code>http://localhost:3000/studio</code>. The Studio picks up the new types and the document list now shows Post, Author, and Category. Create one of each. The reference fields will let you link a post to the author and category you just made.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-sanity-studio-nextjs-16-schema-editor.webp" alt="Sanity Studio document editor showing the Cosmic Harmony Festival document with the Name field. Image: Sanity Inc." width="1600" height="1058">
<em>Source: <a href="https://www.sanity.io/learn/course/day-one-with-sanity-studio">Sanity Learn: Day one with Sanity Studio</a></em></p>
<p>This is what the editor looks like for a real document: the list pane on the left, the document with its declared fields on the right, drafts and publish state at the top. With the schema above, your screen looks the same shape, with your post fields in place of the Name shown here.</p>
<h2 id="how-do-you-query-sanity-content-from-a-server-component">How do you query Sanity content from a Server Component?</h2>
<p>Create a thin client wrapper at <code>lib/sanity/client.ts</code>, then import it from any Server Component that needs to render content. The client uses GROQ, Sanity's query language, which feels like JSON Path with projections.</p>
<p>The wrapper is short:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// lib/sanity/client.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next-sanity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> sanityClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createClient</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  projectId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> process</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">env</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">NEXT_PUBLIC_SANITY_PROJECT_ID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  dataset</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> process</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">env</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">NEXT_PUBLIC_SANITY_DATASET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  apiVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2026-06-01</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  useCdn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> allPostsQuery</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> `</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">*[_type == "post"] | order(publishedAt desc) {</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  _id,</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  title,</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  "slug": slug.current,</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  publishedAt,</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  "author": author->name,</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  "category": category->title</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>A few specifics. <code>apiVersion</code> is a calendar date that pins query behavior, so a Sanity-side change does not silently alter your responses. Update it when you adopt a new feature. <code>useCdn: true</code> reads from the public CDN, which is fast and free for published content. Set it to <code>false</code> for draft previews where you want fresh data.</p>
<p>GROQ itself is compact. <code>*[_type == "post"]</code> filters every document by type. The <code>|</code> pipe sorts. The projection block at the end picks fields and rewrites them: <code>"slug": slug.current</code> flattens the slug object to a string, and <code>"author": author->name</code> follows the reference and pulls the author's name in one query.</p>
<p>A Server Component that lists posts is then just:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// app/blog/page.tsx</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> sanityClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> allPostsQuery</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/lib/sanity/client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PostRow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#E36209">  _id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#E36209">  title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#E36209">  slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#E36209">  publishedAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#E36209">  author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#E36209">  category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> BlogIndexPage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> posts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> sanityClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fetch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">PostRow</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">allPostsQuery</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">ul</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">posts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">li</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">a</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> href</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/blog/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">a</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            by </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">author</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> in </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">li</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      ))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">ul</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This is a Server Component, so the query runs at request time on Vercel, not in the browser. The response is HTML by the time it reaches the user. For static caching, swap to Cache Components with <code>'use cache'</code> and a <code>cacheTag</code> keyed off <code>post</code>, and trigger revalidation from a Sanity webhook on publish.</p>
<h2 id="how-do-you-configure-cors-so-the-studio-talks-to-your-project">How do you configure CORS so the Studio talks to your project?</h2>
<p>Open <code>manage.sanity.io</code>, pick your project, go to <strong>API</strong>, scroll to <strong>CORS origins</strong>, and add every origin where the embedded Studio will run. Each entry needs the "Allow credentials" toggle turned on.</p>
<p>The minimum allowlist for a Vercel-hosted Next.js app is three rows:</p>
<table>
<thead>
<tr>
<th>Origin</th>
<th>When it matters</th>
<th>Allow credentials</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>http://localhost:3000</code></td>
<td>Local dev</td>
<td>ON</td>
</tr>
<tr>
<td><code>https://www.your-domain.com</code></td>
<td>Production</td>
<td>ON</td>
</tr>
<tr>
<td><code>https://preview-*.your-domain.vercel.app</code></td>
<td>Vercel preview URLs</td>
<td>ON</td>
</tr>
</tbody>
</table>
<p>The reason "Allow credentials" must be ON is that the embedded Studio sends the Sanity editor session cookie on every API call. Without that toggle, the browser strips the cookie before sending the request, Sanity sees an anonymous call, and you get a blank Studio with a console error like <code>CORS policy: No 'Access-Control-Allow-Credentials' header is present on the requested resource</code>. It is the single most common failure when first setting up the embedded Studio.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-set-up-sanity-studio-nextjs-16-cors.webp" alt="Sanity manage console CORS origins panel. Three rows are listed with Allow credentials toggled ON: http://localhost:3000, https://www.your-domain.com, and https://preview-*.your-domain.vercel.app. A red callout explains why Allow credentials must be on." width="1600" height="900"></p>
<p>Restart <code>npm run dev</code> after adding the localhost entry. The dev server picks up the new CORS allowance on the next page load, and the Studio finishes mounting instead of stalling on the login screen.</p>
<h2 id="how-do-you-deploy-the-embedded-studio-to-vercel">How do you deploy the embedded Studio to Vercel?</h2>
<p>Add the two <code>NEXT_PUBLIC_SANITY_*</code> env vars to your Vercel project, push the branch, and the embedded Studio is reachable at <code>your-domain.com/studio</code> as soon as the deploy goes green. There is no separate Sanity build step.</p>
<p>The full Vercel checklist:</p>
<ol>
<li><strong>Environment variables.</strong> In the Vercel dashboard, go to your project, then <strong>Settings</strong>, then <strong>Environment Variables</strong>. Add <code>NEXT_PUBLIC_SANITY_PROJECT_ID</code> and <code>NEXT_PUBLIC_SANITY_DATASET</code> for all three environments (Production, Preview, Development). Add <code>SANITY_API_READ_TOKEN</code> only if you wired the draft preview flow.</li>
<li><strong>CORS for production and preview URLs.</strong> Back in <code>manage.sanity.io</code>, make sure both <code>https://www.your-domain.com</code> and <code>https://*-your-team.vercel.app</code> are in the CORS allowlist with credentials on. Preview URLs change per commit, so the wildcard saves you from re-adding them on every branch.</li>
<li><strong>Push.</strong> The next push triggers a Vercel build. The build compiles the Studio bundle as part of the Next.js build and ships it as a static route, so cold starts are not part of the editor experience.</li>
<li><strong>Test the editor.</strong> Open <code>https://www.your-domain.com/studio</code>, sign in, create a document, and confirm the content shows up on the public route.</li>
</ol>
<p>You can skip the hosted <code>sanity.studio</code> deployment entirely. That product is useful when you want the editor on a different domain from the marketing site (for example, when the marketing site is on a different stack), but for a single Next.js app, the embedded route is fewer moving parts.</p>
<h2 id="what-are-the-common-pitfalls-in-a-nextjs-16--sanity-setup">What are the common pitfalls in a Next.js 16 + Sanity setup?</h2>
<p>Most first-run problems come from four places: missing env vars at build time, missing CORS entries, the wrong <code>apiVersion</code>, and treating the Studio as a normal Next.js route.</p>
<p>In order of how often I have hit them:</p>
<ul>
<li><strong><code>process.env.NEXT_PUBLIC_SANITY_PROJECT_ID</code> is undefined in production.</strong> Vercel does not read <code>.env.local</code>. Set the variables in the dashboard with the <code>NEXT_PUBLIC_</code> prefix exactly, then redeploy. A redeploy is required for env changes to take effect.</li>
<li><strong>CORS blank screen on preview URLs.</strong> Vercel preview URLs change per branch. Add <code>https://*-your-team.vercel.app</code> once instead of one entry per branch.</li>
<li><strong>Stale data after publishing.</strong> If you have ISR or Cache Components on the read path, publish events do not invalidate them automatically. Wire a Sanity webhook to a Next.js route that calls <code>revalidateTag</code> or <code>updateTag</code> on publish.</li>
<li><strong>Studio is wrapped in your site layout.</strong> The catch-all route under <code>app/studio/[[...tool]]/page.tsx</code> inherits any layout above it. If your root layout adds a navbar or padding, the Studio renders inside it. Wrap the Studio route in its own segment with a layout that returns the children as-is, so the editor takes the full viewport.</li>
</ul>
<p>If you build the site on the same Next.js 16 stack I write about, you have probably already seen related setups in <a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">the proxy.ts post about routing middleware</a> and <a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">the docs generator post about content-driven pages</a>. The Sanity Studio embed is the read/write half of the same problem those posts cover from the routing and rendering sides.</p>
<h2 id="what-did-we-just-build">What did we just build?</h2>
<p>A real CMS dashboard at <code>/studio</code> inside a Next.js 16 app, backed by a typed schema for posts, authors, and categories, queried from Server Components, with CORS configured for local and production, deployed on the same Vercel build as the public site. No extra infrastructure, no second domain, no separate auth flow. The whole thing fits in two config files, one route, and a <code>sanity/</code> directory.</p>
<p>The next step depends on what you ship next. If editors will write drafts you want to preview before publishing, set up the Presentation tool and a draft-mode route on the Next.js side. If you want to render rich text from the <code>body</code> field, install <code>@portabletext/react</code> and write a custom renderer for your design system. If you want full-text search, add <code>searchTool</code> to the plugins list and you get a search bar in the Studio header for free.</p>
<p>For reference, the primary docs I used while writing this are the <a href="https://www.sanity.io/docs/nextjs">official Next.js integration guide</a>, the <a href="https://www.sanity.io/docs/studio/embedding-sanity-studio">embedding Sanity Studio doc</a>, the <a href="https://www.sanity.io/docs/studio/installation">Sanity Studio installation requirements</a>, the <a href="https://github.com/sanity-io/next-sanity">next-sanity toolkit on GitHub</a>, and the <a href="https://www.sanity.io/docs/changelog/06f976e4-865b-41df-a96c-3daca52640a3">Sanity platform changelog covering the Next.js 16 migration</a>. The Studio screenshots in this post are from the <a href="https://www.sanity.io/learn/course/day-one-with-sanity-studio">Day One with Sanity Studio learn course</a> on the Sanity site.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">Hello, proxy.ts in Next.js 16: middleware renamed and reframed</a></li>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a modern docs generator in Next.js 16</a></li>
<li><a href="https://www.rabinarayanpatra.com/blogs/nextjs-16-2-agents-md-next-browser">Next.js 16.2: AGENTS.md and the Next browser</a></li>
<li><a href="https://www.rabinarayanpatra.com/blogs/build-mcp-app-interactive-ui">How to build an interactive MCP app with the MCP Apps SDK</a></li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Version REST APIs in Spring Framework 7 (Spring Boot 4)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-to-version-rest-apis-spring-framework-7</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-to-version-rest-apis-spring-framework-7</guid>
      <pubDate>Tue, 02 Jun 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Spring Framework 7 API versioning is built in. Learn the version attribute, all four resolver strategies, baseline versions, and Sunset deprecation headers.]]></description>
      <content:encoded><![CDATA[<p>For years, versioning a Spring REST API meant picking your own poison. Custom interceptors, duplicate controllers, or a pile of <code>headers=</code> conditions on every mapping. Spring Framework 7, the core of Spring Boot 4, ends that. Versioning is now a first-class feature baked into request mapping.</p>
<p>I rebuilt a multi-version API on Spring Boot 4 last month and deleted about 300 lines of routing glue in the process. This guide is the playbook I wish I'd had: every strategy, real controller code, a <code>curl</code> trace per approach, and the deprecation headers that tell clients when to move on.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-version-rest-apis-spring-framework-7.webp" alt="Spring Framework 7 API versioning overview showing a versioned request routed to the correct controller handler" width="1600" height="896"></p>
<h2 id="what-changed-for-api-versioning-in-spring-framework-7">What changed for API versioning in Spring Framework 7?</h2>
<p>Spring Framework 7 added a <code>version</code> attribute to <code>@RequestMapping</code> and every shortcut variant, so one path can fan out to multiple handlers by version. Before this, request mapping had no concept of a version at all. You bolted versioning on from the outside with interceptors or separate controller classes per release.</p>
<p>The new model has three moving parts. A resolver pulls the version out of the request. A parser turns that raw string into a comparable semantic version. A strategy matches it against the <code>version</code> declared on each mapping and picks the closest handler. All three are configurable, and the defaults cover the common cases.</p>
<p>Here's the smallest possible example. Same path, two versions, two methods:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/accounts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> AccountController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AccountV1</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getAccountV1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> accountService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findV1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AccountV2</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getAccountV2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> accountService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findV2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>A request for version <code>1.0</code> hits the first method. A request for <code>2.0</code> hits the second. No <code>if</code> checks, no manual parsing, no shared dispatcher method. The version is part of the mapping, exactly like the path and the HTTP method.</p>
<h2 id="how-do-you-turn-on-api-versioning-in-spring-boot-4">How do you turn on API versioning in Spring Boot 4?</h2>
<p>You turn on versioning by implementing <code>WebMvcConfigurer</code> and overriding <code>configureApiVersioning</code>, which hands you an <code>ApiVersionConfigurer</code>. Nothing routes by version until you declare which resolver to use. The <code>version</code> attribute on a mapping is inert without a configured strategy.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ApiVersioningConfig</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> WebMvcConfigurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> configureApiVersioning</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ApiVersionConfigurer</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">useRequestHeader</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                  .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addSupportedVersions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That's the entire wiring for header-based versioning. <code>useRequestHeader</code> names the header to read. <code>addSupportedVersions</code> declares the versions you accept, which lets Spring reject anything unknown with a clean 400 instead of a confusing 404.</p>
<p>If you'd rather stay in properties, Spring Boot 4 exposes the same switch:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="properties" data-theme="material-theme github-light"><code data-language="properties" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.mvc.apiversion.use.header</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">X-API-Version</span></span></code></pre></figure>
<p>By default a version is required. A request with no version triggers <code>MissingApiVersionException</code> and a 400 response. An unsupported version triggers <code>InvalidApiVersionException</code>, also a 400. You can relax both, and I'll cover that under pitfalls, because the default trips up first-time users.</p>
<h2 id="how-does-the-version-attribute-route-requests">How does the version attribute route requests?</h2>
<p>The <code>version</code> attribute routes a request by comparing the resolved request version against the version declared on each candidate mapping, then choosing the best match. Spring parses both sides with <code>SemanticApiVersionParser</code>, which reads <code>major.minor.patch</code> and fills missing parts with zero. So <code>"1"</code> becomes <code>1.0.0</code> and <code>"1.2"</code> becomes <code>1.2.0</code>.</p>
<p>Matching is not a naive string equality. If a request asks for <code>1.5</code> and your handlers declare <code>1.0</code> and <code>2.0</code>, the request resolves to the highest version less than or equal to <code>1.5</code>, which is <code>1.0</code>. That single rule is what makes baseline versioning work, and it's why you don't need a handler for every point release.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-version-rest-apis-spring-framework-7-routing.webp" alt="Diagram of the Spring Framework 7 version resolution flow from request to resolver to parser to matched handler" width="1600" height="900"></p>
<p>The flow is the same no matter which strategy you choose. Only the first box, the resolver, changes. Pick where the version lives in the request and the rest of the pipeline stays identical.</p>
<h2 id="which-versioning-strategy-should-you-pick">Which versioning strategy should you pick?</h2>
<p>Spring Framework 7 ships four resolver strategies, and you choose one with a single method on <code>ApiVersionConfigurer</code>. Each reads the version from a different place in the request. The handler code never changes. Only the config line and the way clients call you differ.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-version-rest-apis-spring-framework-7-strategies.webp" alt="Comparison table of the four Spring Framework 7 API versioning strategies: path segment, request header, query parameter, and media type" width="1600" height="900"></p>
<h3 id="request-header">Request header</h3>
<p>Header versioning keeps the URL clean and puts the version in a custom header. This is my default for service-to-service APIs.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">useRequestHeader</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addSupportedVersions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -H</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-Version: 2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> http://localhost:8080/accounts/42</span></span></code></pre></figure>
<h3 id="path-segment">Path segment</h3>
<p>Path-segment versioning puts the version directly in the URL, which makes it visible, bookmarkable, and easy to route at a proxy. You declare which segment holds the version by index and add a URI variable for it in the mapping.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">usePathSegment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addSupportedVersions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/{version}/accounts/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AccountV2</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getAccount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> accountService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findV2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> http://localhost:8080/api/2.0/accounts/42</span></span></code></pre></figure>
<p>Segment index is zero-based against the path. In <code>/api/2.0/accounts/42</code> the segments are <code>api</code>, <code>2.0</code>, <code>accounts</code>, <code>42</code>, so the version sits at index <code>1</code>.</p>
<h3 id="query-parameter">Query parameter</h3>
<p>Query-parameter versioning reads the version from the query string. It's trivial to test in a browser and trivial to forget in a cache key, so weigh that tradeoff.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">useQueryParam</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addSupportedVersions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">http://localhost:8080/accounts/42?version=2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<h3 id="media-type">Media type</h3>
<p>Media-type versioning reads a parameter off the <code>Accept</code> header, which is the most REST-purist option and the hardest for casual clients to send.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">useMediaTypeParameter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">MediaType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">APPLICATION_JSON</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addSupportedVersions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -H</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Accept: application/json;version=2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> http://localhost:8080/accounts/42</span></span></code></pre></figure>
<p>My rule of thumb: header for internal APIs, path segment for public APIs where humans read the URLs, and skip query and media type unless you have a specific reason. The worst choice is no choice, where different endpoints use different strategies and clients can't predict which one applies.</p>
<h2 id="whats-the-difference-between-fixed-and-baseline-versions">What's the difference between fixed and baseline versions?</h2>
<p>A fixed version matches one exact version, while a baseline version matches that version and everything above it until a higher handler takes over. You write a fixed version as <code>"1.2"</code> and a baseline version with a trailing plus, <code>"1.2+"</code>. The difference is how many releases a single handler is responsible for.</p>
<p>Picture an endpoint that hasn't changed since <code>1.0</code> and another that got a new shape in <code>2.0</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/accounts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> AccountController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0+</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AccountV1</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getAccount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> accountService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findV1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PostMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0+</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AccountV2</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createAccount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestBody</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CreateAccount</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> accountService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createV2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The <code>GET</code> handler answers <code>1.0</code>, <code>1.5</code>, <code>1.9</code>, and anything up to the next declared version. The <code>POST</code> handler owns <code>2.0</code> and up. You only write a new method when the contract actually changes, not on every version bump. That's the whole point of baseline matching, and it's why a real API with ten releases might have three or four handlers per route instead of ten.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-version-rest-apis-spring-framework-7-baseline.webp" alt="Diagram contrasting a fixed version that matches one release against a baseline version that matches a range of releases" width="1600" height="900"></p>
<p>Fixed versions still matter. Use them when a single release introduced a breaking change you want pinned to one exact handler, so a typo in a client's version string fails loudly instead of silently falling through to an older shape.</p>
<h2 id="how-do-you-deprecate-an-api-version-with-sunset-headers">How do you deprecate an API version with Sunset headers?</h2>
<p>You deprecate a version by registering a <code>StandardApiVersionDeprecationHandler</code> and configuring a deprecation date, a sunset date, and a migration link per version. Spring then attaches three response headers to every matching request: <code>Deprecation</code> from RFC 9745, <code>Sunset</code> from RFC 8594, and a <code>Link</code> pointing at your migration docs. Clients that watch for these headers learn a version is going away without reading your changelog.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ApiVersioningConfig</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> WebMvcConfigurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> configureApiVersioning</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ApiVersionConfigurer</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        StandardApiVersionDeprecationHandler</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> handler </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> StandardApiVersionDeprecationHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        handler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">configureVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">               .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setDeprecationDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ZonedDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">parse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2026-06-01T00:00:00Z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">               .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setSunsetDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ZonedDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">parse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2026-12-01T00:00:00Z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">               .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setSunsetLink</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://api.example.com/docs/migrate-to-2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">useRequestHeader</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                  .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addSupportedVersions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                  .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setDeprecationHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">handler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Now a call to the deprecated version carries the warning in its response:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">curl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -i</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -H</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-Version: 1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> http://localhost:8080/accounts/42</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="http" data-theme="material-theme github-light"><code data-language="http" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">HTTP</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1.1</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 200</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> OK</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Deprecation</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Mon, 01 Jun 2026 00:00:00 GMT</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Sunset</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Tue, 01 Dec 2026 00:00:00 GMT</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Link</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> &#x3C;https://api.example.com/docs/migrate-to-2.0>; rel="sunset"</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">Content-Type</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> application/json</span></span></code></pre></figure>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/how-to-version-rest-apis-spring-framework-7-deprecation.webp" alt="HTTP response trace showing Deprecation, Sunset, and Link headers emitted by Spring Framework 7 for a deprecated API version" width="1600" height="900"></p>
<p>The version still works. Deprecation is a signal, not a shutdown. When the sunset date passes and you're confident traffic has moved, you drop the <code>1.0</code> handler and remove <code>"1.0"</code> from the supported versions. Clients that ignored the headers get a clean 400, and you have the access logs to prove you warned them.</p>
<h2 id="how-was-this-done-before-spring-7">How was this done before Spring 7?</h2>
<p>Before Spring 7 you versioned by hand, and every approach had a sharp edge. The most common pattern was duplicate controllers, one class per version, wired to different base paths:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/v1/accounts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> AccountControllerV1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> /* ... */</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/v2/accounts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> AccountControllerV2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> /* ... */</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>That works until you have twenty endpoints across four versions and a bug fix has to land in three of them. The other common hack abused the <code>headers</code> condition on a mapping:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/accounts/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-Version=1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AccountV1</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getV1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> /* ... */</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>String equality only. No <code>1.0</code> versus <code>1</code> normalization, no baseline ranges, no <code>1.5</code> falling back to <code>1.0</code>. You hand-rolled every comparison. Teams that wanted real semantics wrote a custom <code>HandlerInterceptor</code> to parse and validate versions, then threaded the result through <code>ThreadLocal</code> or request attributes. It was a lot of code to maintain, and it lived nowhere near the mappings it controlled.</p>
<p>The built-in approach wins on every axis I care about. Versioning lives on the mapping where you can see it, comparison is semantic, baseline matching cuts handler count, and deprecation headers come free. The 300 lines I deleted were exactly this kind of glue.</p>
<h2 id="what-pitfalls-should-you-watch-for">What pitfalls should you watch for?</h2>
<p>The first pitfall is the required-version default, which surprises everyone. A request with no version returns a 400, not your newest handler. If you want missing versions to fall back instead of failing, set a default and mark versions optional:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">useRequestHeader</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addSupportedVersions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setVersionRequired</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setDefaultVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>With <code>setVersionRequired(false)</code> and no default, Spring falls back to the most recent supported version, which may not be what you want during a migration. Always pair optional versions with an explicit <code>setDefaultVersion</code> so the fallback is a decision, not an accident.</p>
<p>The second pitfall is supported-version detection. By default Spring initializes the supported set from the versions it finds on your controller mappings, so a typo like <code>version = "20"</code> silently becomes a supported version. If you want a locked allowlist, call <code>detectSupportedVersions(false)</code> and declare every version with <code>addSupportedVersions</code> yourself. I do this on public APIs so nothing ships a version by accident.</p>
<p>The third pitfall is OpenAPI tooling. As of mid-2026, springdoc support for the new <code>version</code> attribute is still catching up, so two handlers on the same path can confuse schema generation. Until your springdoc version understands the version dimension, group your docs per version or document the version header manually in your OpenAPI config. Test your generated spec before you assume it rendered both versions.</p>
<p>The last one is strategy drift. Once you pick a resolver, every endpoint must use it. A header-versioned API with one path-versioned endpoint will route inconsistently and break client SDKs that assume one scheme. Decide the strategy at the start of the project and enforce it in review.</p>
<h2 id="is-built-in-versioning-worth-migrating-to">Is built-in versioning worth migrating to?</h2>
<p>Yes, and it changes how you think about API evolution, not just how you wire it. When adding a version is one attribute and one method, you stop dreading breaking changes and start shipping them cleanly, because the cost of carrying an old contract next to a new one dropped to almost nothing. That's the real shift in Spring Framework 7. The mechanics are simple. The freedom they buy is the point.</p>
<p>Start with header versioning and a required version on a single endpoint. Add a <code>2.0</code> handler with a baseline range. Wire a deprecation handler with a sunset date six months out. Once that loop feels natural, you'll version everything this way and wonder how you tolerated the interceptor era.</p>
<p>For the authoritative details, read the official <a href="https://spring.io/blog/2025/09/16/api-versioning-in-spring">Spring blog announcement on API versioning</a> and the <a href="https://docs.spring.io/spring-framework/reference/web/webmvc-versioning.html">Spring Framework reference on MVC API versioning</a>. For the header semantics, see <a href="https://www.rfc-editor.org/rfc/rfc9745.html">RFC 9745 (Deprecation)</a> and <a href="https://www.rfc-editor.org/rfc/rfc8594.html">RFC 8594 (Sunset)</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-cors-guide">How to Configure CORS in Spring Boot</a>. Get cross-origin headers right before you expose a versioned API.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide">How to Test Spring Boot with Testcontainers</a>. Write integration tests that exercise each API version against a real database.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution">Spring Security's Component Revolution</a>. Secure the versioned endpoints you just built.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java with Spring Boot</a>. The language features that make Spring Boot 4 controllers cleaner.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How to Set Up an SSH Tunnel for Local Database Access]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/ssh-tunnel-local-database-access</link>
      <guid>https://www.rabinarayanpatra.com/blogs/ssh-tunnel-local-database-access</guid>
      <pubDate>Thu, 28 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Set up an SSH tunnel to reach a remote Postgres, MySQL, or Redis from your laptop without exposing the database port. With autossh and a systemd unit.]]></description>
      <content:encoded><![CDATA[<p>The single fastest way to wake up to a ransom note is to expose your Postgres port directly to the public internet. The second fastest is to think you can secure it with a strong password and call it a day.</p>
<p>I have run point on database access for production systems at three different companies. Every single one of them had at least one engineer at some point ask if we could "just open 5432 to my IP" so they could pull a quick report from DBeaver. The answer is always no. The right answer is an SSH tunnel, and once you have done it twice, it takes about 15 seconds to set up.</p>
<p>This post is the practical guide I wish I had given that engineer the first time. We will cover the command, the GUI client setup for the four databases I touch most often, the autossh recipe that keeps the tunnel alive through laptop sleeps and network changes, and the small set of mistakes that bite people in production. If you want the deeper context on why exposing database ports is bad even with strong auth, I covered that in my <a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">zero-trust microservices post</a>, and the connection pooling tradeoffs once you are inside the tunnel show up in <a href="https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide">my pgbouncer survival guide</a>.</p>
<h2 id="what-is-an-ssh-tunnel-and-why-use-it-for-database-access">What is an SSH tunnel and why use it for database access?</h2>
<p>An SSH tunnel for database access forwards a local TCP port on your laptop through an authenticated SSH session to a remote host, which then opens a connection to the actual database. Your database client connects to <code>localhost</code> and never knows anything else exists. All traffic rides inside the encrypted SSH channel.</p>
<p>The shape of it looks like this.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/ssh-tunnel-local-database-access-flow.webp" alt="SSH tunnel flow from laptop to bastion to database" width="1600" height="905"></p>
<p>This pattern wins on five things at once.</p>
<p><strong>No exposed database port.</strong> The database listens only on its private network. The bastion is the only thing on the public internet, and it only speaks SSH.</p>
<p><strong>No firewall holes per developer.</strong> Every engineer goes through the same bastion. You add or remove access by adding or removing SSH keys, not by editing security group rules.</p>
<p><strong>Audit trail.</strong> Every connection logs through SSH and through the bastion's auth.log. You know who connected, from where, when.</p>
<p><strong>Works with every client.</strong> psql, DBeaver, TablePlus, DataGrip, mysql, redis-cli, mongosh. They all just see <code>localhost</code>. No driver-level config needed.</p>
<p><strong>Encrypted in transit by default.</strong> Even if your database speaks plaintext on the wire (looking at you, Redis), the tunnel carries it under SSH.</p>
<p>The one thing it does not give you is high availability. The tunnel is a long-lived TCP connection between exactly two hosts. If your bastion goes down, your tunnel goes with it. That is fine for ad-hoc developer access. It is not fine for application traffic. Application traffic belongs on a private network or a managed bastion service like AWS Session Manager.</p>
<h2 id="how-do-you-set-up-a-local-port-forward-to-a-remote-postgres">How do you set up a local port forward to a remote Postgres?</h2>
<p>The full command is one line.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ssh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 5433:dbhost.internal:5432</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ec2-user@bastion.example.com</span></span></code></pre></figure>
<p>Read it left to right.</p>
<ul>
<li><code>-L 5433:dbhost.internal:5432</code> says forward local port <code>5433</code> on this laptop, through the SSH connection, to <code>dbhost.internal:5432</code> resolved from the bastion's perspective.</li>
<li><code>ec2-user@bastion.example.com</code> is the SSH connection itself.</li>
</ul>
<p>While that command is running, anything on your laptop that connects to <code>localhost:5433</code> ends up talking to the Postgres on <code>dbhost.internal:5432</code>. Close the terminal or hit Ctrl+C and the tunnel dies.</p>
<p>I deliberately use <code>5433</code> on the local side instead of <code>5432</code>. If you happen to have Postgres running locally for development, you do not want to clobber it. Pick a high port that does not collide.</p>
<p>To test the tunnel works without firing up a GUI, use psql from another terminal.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">psql</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -h</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> localhost</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -p</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 5433</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -U</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> app_user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -d</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> production</span></span></code></pre></figure>
<p>That <code>-h localhost</code> is the key. Without it, psql tries to connect via Unix socket and skips the tunnel entirely. I have lost 20 minutes to this twice.</p>
<h3 id="running-the-tunnel-in-the-background">Running the tunnel in the background</h3>
<p>If you do not want a terminal sitting open, add <code>-f -N</code> and the command returns immediately while the tunnel keeps running.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ssh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -N</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 5433:dbhost.internal:5432</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ec2-user@bastion.example.com</span></span></code></pre></figure>
<ul>
<li><code>-N</code> means do not execute a remote command (we only want the forward).</li>
<li><code>-f</code> means fork into the background after authentication.</li>
</ul>
<p>To kill it later, find and stop the process.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">pgrep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -af</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ssh.*5433:dbhost.internal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">kill</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x3C;</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">pi</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">d</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span></span></code></pre></figure>
<h3 id="a-tidier-setup-with-sshconfig">A tidier setup with <code>~/.ssh/config</code></h3>
<p>Typing that command every time gets old. Stash the whole thing in your SSH config.</p>
<pre><code># ~/.ssh/config
Host pg-prod
    HostName bastion.example.com
    User ec2-user
    IdentityFile ~/.ssh/keys/prod-bastion.pem
    LocalForward 5433 dbhost.internal:5432
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes
</code></pre>
<p>Now the command is just <code>ssh pg-prod</code>, and you get a few useful behaviors for free.</p>
<ul>
<li><code>ServerAliveInterval 30</code> sends a keepalive every 30 seconds so the tunnel does not die when your home router decides to drop idle connections.</li>
<li><code>ExitOnForwardFailure yes</code> makes ssh fail fast if the forward cannot bind, instead of leaving you with a dead tunnel and no error.</li>
</ul>
<p>For the background form, <code>ssh -f -N pg-prod</code> still works. The config entry is purely additive.</p>
<h2 id="how-do-you-connect-from-your-gui-client-through-the-tunnel">How do you connect from your GUI client through the tunnel?</h2>
<p>With the tunnel running, every GUI client connects to <code>localhost</code> on the forwarded port. The only field that matters is the host. Everything else (username, password, database name) is the same as you would use against the real database.</p>
<p>In DBeaver, create a new Postgres connection and fill in:</p>
<pre><code>Host:     localhost
Port:     5433
Database: production
Username: app_user
Password: &#x3C;your db password>
</code></pre>
<p>Hit Test Connection. If it works, you are good.</p>
<p>In TablePlus, same idea. Host is <code>localhost</code>, port is <code>5433</code>.</p>
<p>In DataGrip, same. The driver does not know the tunnel exists, which is exactly the point.</p>
<p>DBeaver and DataGrip both have a built-in SSH tunnel option in their connection dialog. That works too. The advantage of using ssh on the command line instead is that you can share one tunnel across psql, DBeaver, a script, and a notebook at the same time. The advantage of the GUI option is that it dies cleanly when you close the client.</p>
<p>I default to the command line approach because I almost always have multiple things hitting the same database. Pick what fits your workflow.</p>
<h2 id="how-do-you-do-the-same-for-mysql-redis-and-mongodb">How do you do the same for MySQL, Redis, and MongoDB?</h2>
<p>The flag is identical. Only the port changes.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># MySQL</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ssh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 3307:dbhost.internal:3306</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> user@bastion</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Redis</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ssh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 6380:cache.internal:6379</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> user@bastion</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># MongoDB</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ssh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 27018:mongohost.internal:27017</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> user@bastion</span></span></code></pre></figure>
<p>Then connect each client to <code>localhost</code> on the forwarded port.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">mysql</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -h</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 127.0.0.1</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -P</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 3307</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> app_user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -p</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">redis-cli</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -h</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 127.0.0.1</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -p</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 6380</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">mongosh</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">mongodb://app_user:secret@127.0.0.1:27018/production</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>A few client-specific notes that catch people.</p>
<p><strong>MySQL needs <code>127.0.0.1</code>, not <code>localhost</code>.</strong> The mysql client tries to connect via Unix socket when you say <code>localhost</code>, just like psql does. Use the IP literal and you skip that.</p>
<p><strong>Redis tunnels work great for debugging, badly for sustained throughput.</strong> Every command round-trips through the SSH session. Latency goes from 0.2 ms to 8-30 ms depending on your link. Fine for <code>redis-cli MONITOR</code> or one-off lookups. Wrong tool for ETL.</p>
<p><strong>Mongo replica sets need extra care.</strong> A standalone mongod tunnels fine. A replica set will hand you back the internal hostnames of the other replicas during connection negotiation, and your client will then try to connect to those names directly. Either tunnel each replica on its own port and add them to your connection string, or set <code>directConnection=true</code> in the URI to disable replica discovery.</p>
<h2 id="how-do-you-keep-the-tunnel-alive-with-autossh-and-systemd">How do you keep the tunnel alive with autossh and systemd?</h2>
<p><code>ssh -f -N</code> works until your laptop goes to sleep, your wifi switches, or the bastion restarts. Then the tunnel dies silently and your next connection just hangs. The fix is autossh, which is a tiny wrapper that monitors the SSH process and restarts it when it drops.</p>
<p>Install it.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># macOS</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">brew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> autossh</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Ubuntu / Debian</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> apt</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> autossh</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Fedora / Rocky</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> dnf</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> autossh</span></span></code></pre></figure>
<p>Run it the same way you ran ssh, with one extra port for autossh's own health check.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">AUTOSSH_GATETIME</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">0</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">autossh </span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">-M</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -N</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -o</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ServerAliveInterval 30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -o</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ServerAliveCountMax 3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -o</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ExitOnForwardFailure yes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 5433:dbhost.internal:5432</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  ec2-user@bastion.example.com</span></span></code></pre></figure>
<p>A few details that matter.</p>
<ul>
<li><code>AUTOSSH_GATETIME=0</code> makes autossh restart immediately even if the first connection failed. Without it, autossh waits 30 seconds before retrying, which is annoying when you mis-typed the host.</li>
<li><code>-M 0</code> disables autossh's old monitoring port mechanism. We use the <code>ServerAlive*</code> SSH options instead, which work better through NATs and modern firewalls.</li>
</ul>
<p>That command stays up through sleep, wake, and network changes. But it does not survive a reboot. For that, wrap it in a systemd user service.</p>
<h3 id="the-systemd-user-service">The systemd user service</h3>
<p>Create <code>~/.config/systemd/user/pg-tunnel.service</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ini" data-theme="material-theme github-light"><code data-language="ini" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[Unit]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Autossh tunnel to production Postgres</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">After</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">network-online.target</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Wants</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">network-online.target</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[Service]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">simple</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Environment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">AUTOSSH_GATETIME</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">0</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">ExecStart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/usr/bin/autossh -M 0 -N \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  -o </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ServerAliveInterval 30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  -o </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ServerAliveCountMax 3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  -o </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ExitOnForwardFailure yes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  -o </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ConnectTimeout 10</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  -i /home/rabi/.ssh/keys/prod-bastion.pem \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  -L 5433:dbhost.internal:5432 \</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  ec2-user@bastion.example.com</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">Restart</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">always</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">RestartSec</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">10</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[Install]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">WantedBy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">default.target</span></span></code></pre></figure>
<p>Notice the ExecStart does not use <code>-f</code> here. Systemd wants the process in the foreground so it can supervise it. The <code>Restart=always</code> line takes over from the <code>-f</code> background behavior.</p>
<p>Enable and start.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> daemon-reload</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> enable</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --now</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pg-tunnel.service</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> status</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pg-tunnel.service</span></span></code></pre></figure>
<p>That last command should show <code>active (running)</code>. If you want the service to start at boot (not just at login), enable user lingering once.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> loginctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> enable-linger</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> $USER</span></span></code></pre></figure>
<p>Logs are in journalctl.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">journalctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pg-tunnel.service</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span></span></code></pre></figure>
<p>On macOS, the same job goes into a launchd plist at <code>~/Library/LaunchAgents/com.rabi.pg-tunnel.plist</code>. The shape is similar enough that I will skip the full XML, but the key entries are <code>KeepAlive</code> set to true and a <code>ProgramArguments</code> array that mirrors the autossh command above.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/ssh-tunnel-local-database-access-systemd.webp" alt="Autossh under systemd keeps the tunnel alive across reboots" width="1600" height="905"></p>
<h2 id="what-are-the-common-ssh-tunnel-mistakes-to-avoid">What are the common SSH tunnel mistakes to avoid?</h2>
<p>I have made every one of these. So has everyone I have onboarded.</p>
<p><strong>Binding the forward to 0.0.0.0 instead of 127.0.0.1.</strong> The default <code>-L 5433:dbhost:5432</code> binds to localhost only. If you write <code>-L *:5433:dbhost:5432</code>, you have just opened your laptop's port 5433 to anyone on the same wifi network as you. They can now talk to your production Postgres. Do not do this. The local bind defaults to localhost for a reason.</p>
<p><strong>Hardcoding production credentials in <code>~/.ssh/config</code>.</strong> SSH config has no concept of secrets management. If you check your config into a dotfiles repo, that key path goes with it. Keep production keys outside the config-tracked area and reference them by absolute path from a directory that is gitignored.</p>
<p><strong>Forgetting that <code>ServerAliveInterval</code> is client-side only.</strong> This tells your laptop to send keepalives. The bastion can still close the connection on its own idle timeout. If your bastion is AWS Systems Manager based or sits behind a corporate load balancer with a short idle, you also need to set keepalives at the server level (<code>ClientAliveInterval</code> in sshd_config). Otherwise your tunnel drops every five minutes and you do not know why.</p>
<p><strong>Trusting the tunnel to give you encryption end-to-end.</strong> SSH encrypts the laptop-to-bastion segment. The bastion-to-database segment is in cleartext on your private network. For most production setups, that is fine because the private network is trusted. For sensitive workloads with stricter compliance requirements (PCI, HIPAA), insist on TLS on the database side too, even inside the VPC.</p>
<p><strong>Tunneling through your jump host as root.</strong> The bastion is a public-facing box. The account that holds your tunnel should not also be the one with sudo rights. Use a dedicated low-privilege user for tunneling. Restrict it to <code>command="false",no-shell</code> in <code>authorized_keys</code> if you want to be paranoid, with a <code>PermitOpen</code> directive that limits which <code>host:port</code> combos this key can forward to.</p>
<p>The hardened authorized_keys line looks like this.</p>
<pre><code>command="echo 'no shell',no-pty,no-agent-forwarding,no-X11-forwarding,permitopen="dbhost.internal:5432" ssh-ed25519 AAAA... rabi@laptop
</code></pre>
<p>That key can open exactly one forward, to exactly one host:port, and cannot execute a shell. If the key leaks, the attacker gets a tunnel to one database, not a shell on your bastion.</p>
<p><strong>Letting the tunnel persist across job changes.</strong> This one is process, not technical. When an engineer leaves the team, their SSH keys come out of every bastion's <code>authorized_keys</code>. The keys are in your <a href="https://www.rabinarayanpatra.com/snippets/aws-cli/secrets-manager-get">secrets manager</a> and config-managed. Right? Right.</p>
<p><strong>Using the same bastion for staging and production.</strong> I have seen this break twice. A misconfigured <code>LocalForward</code> ends up pointing your TablePlus at production while you think you are on staging. Use separate bastions, separate config entries with distinct host names like <code>pg-prod</code> and <code>pg-stage</code>, and color-code the connections in your client so you cannot confuse them visually.</p>
<h2 id="what-does-the-whole-setup-look-like-in-30-seconds">What does the whole setup look like in 30 seconds?</h2>
<p>For the reader who skipped to the end, here is the compressed version.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># One-off</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ssh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -N</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 5433:dbhost.internal:5432</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> user@bastion</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">psql</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -h</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> localhost</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -p</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 5433</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -U</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> app_user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -d</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> production</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Persistent, survives sleep and network changes</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">brew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> autossh</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">AUTOSSH_GATETIME</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">0</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> autossh</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -M</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -f</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -N</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -o</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ServerAliveInterval 30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -L</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 5433:dbhost.internal:5432</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> user@bastion</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Persistent, survives reboot (Linux)</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Drop the autossh command into ~/.config/systemd/user/pg-tunnel.service</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># and run: systemctl --user enable --now pg-tunnel.service</span></span></code></pre></figure>
<p>That is genuinely all you need for 95% of the database access situations a developer hits.</p>
<p>The whole reason this pattern works is that SSH is older than most of the protocols we layer on top of it, and the port-forwarding feature has been battle-tested for three decades. Use it. Stop opening database ports to the internet. Your future self will thank you the next time a Shodan-driven exploit campaign sweeps the internet looking for misconfigured Postgres instances, which is approximately every Tuesday.</p>
<p>For more on the protocol, see the <a href="https://man.openbsd.org/ssh.1">OpenSSH manual page for ssh(1)</a>, the <a href="https://www.harding.motd.ca/autossh/">autossh project page</a>, and <a href="https://datatracker.ietf.org/doc/html/rfc4254">RFC 4254</a> for the SSH connection protocol spec that defines port forwarding.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide">Postgres Connection Pool: A pgbouncer Survival Guide</a>: once you can reach the database, the next question is how many connections you can hold open without melting it.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero-Trust Microservices with Spring Security</a>: the bigger frame for why exposing internal ports is the wrong default, even inside a private network.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Spring AI 2.0 MCP Server Tutorial: @McpTool to Prod]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/spring-ai-2-mcp-annotations-tutorial</link>
      <guid>https://www.rabinarayanpatra.com/blogs/spring-ai-2-mcp-annotations-tutorial</guid>
      <pubDate>Tue, 26 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Spring AI 2.0 MCP annotations tutorial: build a production MCP server with @McpTool, async progress reporting, Streamable HTTP transport, and Claude Code wiring.]]></description>
      <content:encoded><![CDATA[<p>Spring AI 2.0.0-M6 dropped on May 8, 2026. Buried in the release notes was the thing I had been waiting for since I first wired an MCP server in Java six months ago: native annotations. <code>@McpTool</code>, <code>@McpResource</code>, <code>@McpPrompt</code>, <code>@McpComplete</code>. All in core. All auto-registered.</p>
<p>If you've written an MCP server with the older Spring AI <code>ToolCallback</code> API, you remember the ritual. Build descriptors, register callbacks, wire up the transport manually, handle JSON schema by hand. The annotation API replaces all of it with a single annotation on a method. Spring AI generates the schema. Auto-configuration handles registration. You write the business logic.</p>
<p>This tutorial walks the full path: building a production MCP server, exposing tools and resources, handling async work with progress reporting, picking a transport, registering with Claude Code, and the gotchas I've hit that nobody mentions on the docs.</p>
<h2 id="what-changed-in-spring-ai-20-mcp-annotations">What changed in Spring AI 2.0 MCP annotations?</h2>
<p>Spring AI 2.0 collapses MCP server and client wiring into a small set of annotations that Spring Boot auto-configuration picks up at startup. The annotated beans become the MCP surface for your Spring Boot app.</p>
<p>The core server annotations are:</p>
<ul>
<li><code>@McpTool</code> marks a method as an MCP tool. Spring AI builds the JSON schema from your method parameters.</li>
<li><code>@McpResource</code> exposes a resource via a URI template.</li>
<li><code>@McpPrompt</code> exposes a prompt template that clients can fetch.</li>
<li><code>@McpComplete</code> provides auto-completion for prompt or resource arguments.</li>
</ul>
<p>The auto-configuration also injects special context parameters like <code>McpSyncRequestContext</code> and <code>McpAsyncRequestContext</code>, which give your method access to logging, progress reporting, sampling, and elicitation without polluting the JSON schema.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/spring-ai-2-mcp-annotations-tutorial-architecture.webp" alt="Spring AI 2.0 MCP annotations layered architecture: Java annotations through Spring Boot auto-configuration to MCP transport" width="1600" height="905"></p>
<p>For a Spring shop, this is the kind of API change that flips a project's complexity. Before, every team building an MCP server wrote the same plumbing. Now the plumbing is gone.</p>
<h2 id="how-do-you-build-an-mcp-server-with-mcptool">How do you build an MCP server with @McpTool?</h2>
<p>Start with a Spring Boot 3.5+ project on Java 21 or higher. Add the MCP server starter to your <code>pom.xml</code>. There are three flavors: stdio/SSE default, WebMVC, and WebFlux. For production HTTP transport, pick WebMVC or WebFlux based on whether you want blocking or reactive code.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">org.springframework.ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">spring-ai-starter-mcp-server-webmvc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">2.0.0-M6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Spring AI 2.0 milestones live in the Spring milestone repository, so add it if you haven't:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">repositories</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">repository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">spring-milestones</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Spring Milestones</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">https://repo.spring.io/milestone</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">repository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">repositories</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Now write a tool. The example everyone reaches for first is a calculator, but let's do something a real MCP client would actually want: fetching a user record from your database.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">McpTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">McpToolParam</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">stereotype</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Component</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserTools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserTools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">users </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">get_user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Fetch a user record by ID. Returns name, email, and signup date.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserDto</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpToolParam</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Numeric user ID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> required</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> userId</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UserDto</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">orElseThrow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IllegalArgumentException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User not found: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That's the whole tool. Spring AI does a few things automatically when it scans this bean:</p>
<p>It builds a JSON schema for the <code>userId</code> parameter using the <code>@McpToolParam</code> description and the Java type. The MCP client sees the schema and shows it as a typed input.</p>
<p>It registers the method with the MCP server runtime under the name <code>get_user</code>. The MCP <code>tools/list</code> request returns it. The <code>tools/call</code> request invokes it.</p>
<p>It serializes the return type using Jackson. <code>UserDto</code> becomes a JSON object in the tool result.</p>
<p>You can also use <code>Map&#x3C;String, Object></code> if you want loose typing, or a record class if you want strict typing with deserialization on the client side. Records are my default. The serialization is predictable, the schema is implicit, and the code is less than a Lombok annotation pile would be.</p>
<h2 id="how-do-you-handle-progress-and-async-work">How do you handle progress and async work?</h2>
<p>The most common reason a tool feels broken in production is that it does real work without telling the client. The client sits there. The user sits there. Eventually something times out. Spring AI 2.0 fixes this with a request context that exposes a progress channel.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">McpTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">context</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">McpSyncRequestContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">springframework</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">stereotype</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Component</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ReportTools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ReportService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ReportTools</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ReportService</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> reports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">reports </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">generate_quarterly_report</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Generate the quarterly revenue report. Takes a few seconds.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ReportResult</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> generateReport</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        McpSyncRequestContext</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpToolParam</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Quarter, e.g. 2026-Q1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> required</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> quarter</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">logging</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Loading transactions for </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> quarter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">progress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">report</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0.1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Loading transactions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> transactions </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">loadTransactions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">quarter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">progress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">report</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0.5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Aggregating</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> aggregated </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">aggregate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">transactions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">progress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">report</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0.9</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Rendering</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">render</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">aggregated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>McpSyncRequestContext</code> does not appear in the JSON schema. Spring AI knows it's a framework parameter and excludes it. The user-facing parameters stay clean. Inside the method you get <code>logging()</code>, <code>progress()</code>, <code>sampling()</code>, and <code>elicitation()</code> channels, all wired to the MCP client transport.</p>
<p>For reactive code, use <code>McpAsyncRequestContext</code> and return a <code>Mono</code> or <code>Flux</code>. The progress channel returns Reactor types so you can compose progress reporting into a reactive chain.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpTool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">async_report</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Generate report asynchronously.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Mono</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ReportResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> asyncReport</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    McpAsyncRequestContext</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpToolParam</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Quarter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> quarter</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reports</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">loadAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">quarter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">doOnNext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">_ </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">progress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">report</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0.5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Aggregated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">subscribe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">flatMap</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">reports</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">renderAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>One thing to flag: the progress channel is not the same thing as streaming results. Progress is metadata. The result is still a single return value. If you want token-by-token streaming, you want sampling, not tools.</p>
<h2 id="how-do-you-expose-prompts-and-resources">How do you expose prompts and resources?</h2>
<p>Tools are the most-used MCP primitive, but prompts and resources are what turn a tool collection into a workspace. Prompts let the client request a prefilled prompt template. Resources let the client browse data your server exposes by URI.</p>
<p>A prompt looks like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> IncidentPrompts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpPrompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">incident_summary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Generate an incident summary from a Jira ticket ID.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> incidentSummary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpToolParam</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Jira ticket ID, e.g. INC-1234</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ticketId</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> """</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            Summarize the incident in ticket %s for an executive audience.</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            Include: timeline, root cause, customer impact, and remediation.</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            """</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">formatted</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ticketId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>When the client asks for the <code>incident_summary</code> prompt with <code>INC-1234</code>, the server returns the rendered string. The client passes it to its model.</p>
<p>Resources are different. They expose data the server holds, keyed by URI:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CustomerResources</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CustomerRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CustomerResources</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CustomerRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> customers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">customers </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">McpResource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        uri</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">customer://{customerId}/profile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Customer profile data including billing address and tier.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CustomerProfile</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> profile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> customerId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">profileFor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">customerId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The client can list resources, pick one, and read it. The URI template is matched on the path variable. Spring AI extracts the <code>customerId</code> and passes it to the method.</p>
<p>The combination of tools, prompts, and resources is what makes MCP feel like an actual application surface rather than a function bag. Tools do work. Prompts give the client wording. Resources expose state.</p>
<h2 id="how-do-you-choose-between-transports-stdio-sse-streamable-http">How do you choose between transports (stdio, SSE, Streamable HTTP)?</h2>
<p>Spring AI supports four transports, and the choice matters more than the docs make it sound.</p>
<p>stdio is for local single-process tools. Claude Code spawns your server as a child process and talks to it over stdin and stdout. Fine for personal tooling. Bad for anything multi-user, multi-tenant, or networked.</p>
<p>SSE was the original HTTP transport for MCP. It's being phased out. Spring AI still ships it for backward compatibility, but new servers should not start there.</p>
<p>Streamable HTTP is the current MCP HTTP transport. Stateful, supports bidirectional notifications, works behind reverse proxies, fits production environments. Use this for any server that isn't strictly local.</p>
<p>Stateless Streamable HTTP is the new variant designed for serverless and horizontally scaled deployments. No session affinity required. The tradeoff is that any context that would have lived in the server session has to come back through the request from the client. If you're deploying to Vercel, Cloud Run, or any autoscaled platform, this is your transport.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/spring-ai-2-mcp-annotations-tutorial-transports.webp" alt="MCP transport comparison: stdio for local, SSE deprecated, Streamable HTTP for production, Stateless Streamable HTTP for serverless" width="1600" height="905"></p>
<p>For Streamable HTTP with WebMVC, the auto-config wires it up at <code>/mcp</code> by default. You can change the path:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        transport</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> streamable-http</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /api/mcp</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-spring-server</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1.0.0</span></span></code></pre></figure>
<p>The name and version show up in the MCP <code>initialize</code> response. Set them to something recognizable. Default Spring AI names look generic in client UIs.</p>
<h2 id="how-do-you-register-and-test-the-server-with-claude-code">How do you register and test the server with Claude Code?</h2>
<p>Once your Spring Boot app runs and exposes Streamable HTTP at <code>/mcp</code>, register it with Claude Code. The config lives in <code>~/.config/claude/mcp.json</code> on macOS and Linux, or via the <code>claude mcp</code> CLI.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">mcpServers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">my-spring-server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">streamable-http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">http://localhost:8080/mcp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Restart Claude Code. Run <code>/mcp</code> inside a session. You should see your tools, prompts, and resources listed. Try calling one. If the call returns and the result shows up in your transcript, the loop is alive.</p>
<p>For local stdio testing, swap the JSON to type <code>stdio</code> and point <code>command</code> at your packaged jar:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">mcpServers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">my-spring-server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">stdio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">-jar</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/path/to/server.jar</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Claude Code will spawn the JVM, talk over stdin and stdout, and shut it down when the session ends. The startup cost is real, somewhere between 2 and 5 seconds depending on your dependencies. For HTTP transport, Spring Boot stays running and connections are cheap.</p>
<h2 id="what-are-the-production-readiness-gotchas">What are the production-readiness gotchas?</h2>
<p>The annotation API hides the complexity. That's a feature for most cases, but it also means a few production concerns aren't obvious until they bite.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/spring-ai-2-mcp-annotations-tutorial-gotchas.webp" alt="Six production gotchas for Spring AI MCP servers: error handling, schema generation, auth, long-running tools, scaling, tool naming" width="1600" height="905"></p>
<p><strong>Error handling.</strong> If your <code>@McpTool</code> method throws, Spring AI wraps the exception and returns an MCP error to the client. Good. But the default error mapping returns the exception message verbatim, which can leak internal details. Customize the error handling with a <code>McpExceptionHandler</code> bean if you're exposing the server to untrusted clients.</p>
<p><strong>Schema generation.</strong> Records and POJOs work. Generic collections work. Nested polymorphic types are where the generator gets cranky. If your tool returns a <code>List&#x3C;Animal></code> where <code>Animal</code> is an interface, expect the schema to be vague. Prefer concrete types for tool return values. Save the polymorphism for internal layers.</p>
<p><strong>Auth.</strong> Spring AI does not ship MCP-specific auth. You wire it through normal Spring Security. For Streamable HTTP, add a security filter chain matching <code>/mcp</code> and validate bearer tokens or API keys there. The Stateless Streamable HTTP transport makes this easier because there's no session to protect, only requests.</p>
<p><strong>Long-running tools.</strong> MCP clients have timeouts. Claude Code's default is generous but not infinite. If your tool runs longer than 30 seconds, use the progress channel aggressively, and consider returning a job handle and exposing a second tool to poll status. Trying to hold open a 5-minute synchronous tool call will end in tears.</p>
<p><strong>Scaling.</strong> Streamable HTTP is stateful per session. Sticky sessions or a shared session store are needed if you scale horizontally. If you want stateless scaling, use the Stateless Streamable HTTP transport and design your tools to be self-contained on each request.</p>
<p><strong>Tool naming.</strong> MCP tool names show up in the client UI. Pick verbs. <code>get_user</code> reads better than <code>userQuery</code>. Consistency matters more than cleverness.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The Spring AI 2.0 MCP annotation API is the kind of upgrade you only notice fully when you look back at the old way. The boilerplate is gone. The schema generation is automatic. The transports are real. The progress and async story is sane.</p>
<p>The part I want more Spring teams to internalize: MCP is no longer an experiment. Every major coding agent speaks it. Every Spring service in your fleet is a potential MCP surface. The cost of exposing your internal APIs to an agent has dropped to almost nothing. Whether that's a good idea is a different conversation, but the technical barrier is now low enough that you should decide on purpose, not by default.</p>
<p>For more on Spring AI MCP, see the <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-annotations-overview.html">Spring AI MCP annotations docs</a>, the <a href="https://spring.io/blog/2026/05/08/spring-ai-1-0-7-1-1-6-2-0-0-M6-available-now/">Spring AI 2.0.0-M6 release post</a>, and the <a href="https://modelcontextprotocol.io/">official MCP spec</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects</a>. When to reach for an MCP server vs a skill or a project.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-code-routines-ci-automation">Claude Code Routines for CI Automation</a>. Wiring agents into CI once your MCP server is live.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java + Spring Boot</a>. The Spring Boot 3.x baseline you need before Spring AI 2.0.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Why MCP 2026-07-28 Spec Drops Sessions and Goes Stateless]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/why-mcp-2026-07-28-spec-goes-stateless</link>
      <guid>https://www.rabinarayanpatra.com/blogs/why-mcp-2026-07-28-spec-goes-stateless</guid>
      <pubDate>Sun, 24 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[MCP 2026-07-28 spec release candidate drops sessions for a stateless protocol core. Here is why it changed, what breaks, and how to plan your migration.]]></description>
      <content:encoded><![CDATA[<p>The MCP team locked the 2026-07-28 spec release candidate on May 21. It is the largest revision since launch and yes, it breaks things. If you are running an MCP server today, you need to understand what changes before July 28 finalizes the spec.</p>
<p>I keep seeing people on X call this "MCP 2.0". It is not. The spec uses date versions, not semver. The official name is the 2026-07-28 specification, and it sits as a release candidate until the final freeze on July 28 2026. The ten-week window between now and then is for SDK maintainers and server authors to validate against real workloads.</p>
<p>The big shift is that MCP is going stateless. The current 2025-11-25 spec treats every client-server connection as a session with handshake, identifier, and lifecycle. The new spec rips most of that out at the protocol layer. Below I walk through what changed, what broke, and whether you should rush your migration or wait.</p>
<h2 id="what-is-the-mcp-2026-07-28-spec-release-candidate">What is the MCP 2026-07-28 spec release candidate?</h2>
<p>The 2026-07-28 release candidate is the next major version of the Model Context Protocol, locked on May 21 2026 and finalizing on July 28 2026. It introduces a stateless protocol core, a formal Extensions framework, the Tasks extension for long-running work, MCP Apps for server-rendered UIs, and OAuth-aligned authorization.</p>
<p>The official MCP roadmap calls this revision "the largest revision of the protocol since launch". Tier 1 SDK maintainers (the official Anthropic-maintained Python and TypeScript SDKs) are expected to ship support within the ten-week validation window. If you maintain a server or build agent infrastructure on top of MCP, this is the version you target next.</p>
<p>A subtle but useful detail: the MCP team shifted its 2026 roadmap from release-milestone organization to priority-area focus. Four priority areas drive the spec now: Transport Evolution and Scalability, Agent Communication, Governance Maturation, and Enterprise Readiness. Almost every concrete change in the 2026-07-28 spec maps directly to one of those four. That alignment matters because it tells you what to expect in the next spec drop too.</p>
<h2 id="why-was-the-2025-11-25-stateful-protocol-hard-to-scale">Why was the 2025-11-25 stateful protocol hard to scale?</h2>
<p>The 2025-11-25 spec required every connection to start with an initialize/initialized handshake. The server returned a session identifier in an Mcp-Session-Id response header, and every subsequent request from that client had to carry the same header so the server could route it back to its session state.</p>
<p>That design is fine for a single-process MCP server running on a developer laptop. It is painful in production. The MCP roadmap calls the problem out directly: "Stateful sessions fight with load balancers, horizontal scaling requires workarounds." When you put a load balancer in front of stateful MCP servers you have three bad options.</p>
<p>You can run sticky sessions, which fail open whenever an instance restarts or scales down. You can share session state in Redis or a similar store, which adds a network hop and a single point of failure to every tool call. Or you can do deep packet inspection at the gateway, reading Mcp-Session-Id out of the header to manually route traffic, which forces every MCP client to know your private cluster topology.</p>
<p>In my own work running an MCP server behind a Vercel Function I hit this within a week. Functions are stateless by design. The first attempt to add MCP routing died on cold starts because the next invocation had no idea what session-id the client was carrying. Working around it meant pushing every session into Upstash Redis and eating the round trip on every tools/list call. Not fun.</p>
<p>The pattern is the same for any horizontally scaled deployment. Cloud Run, AWS Lambda, Kubernetes with HPA, even traditional VMs behind an L7 load balancer all run into the same wall. The old spec quietly assumed long-lived processes with shared memory. The new spec assumes nothing.</p>
<h2 id="how-does-the-stateless-protocol-core-actually-work">How does the stateless protocol core actually work?</h2>
<p>The new spec removes the initialize/initialized handshake and the Mcp-Session-Id header from the base protocol. Each MCP request now carries everything the server needs to route it independently, and clients explicitly thread any state across calls themselves.</p>
<p>The headline change for ops folks: a stateless MCP server can sit behind a plain round-robin load balancer with no sticky sessions and no shared session store. According to the release notes, gateways can route traffic on the new Mcp-Method header instead of inspecting payloads, and clients can cache tools/list responses for as long as the server's ttlMs field permits. That last part matters because in production a tools/list call on a busy server can dominate the latency budget.</p>
<p>Here is the shape of the change in terms of request flow.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>Before (2025-11-25):</span></span>
<span data-line=""><span>client -> initialize -> server -> 200 + Mcp-Session-Id: abc</span></span>
<span data-line=""><span>client -> tools/list (Mcp-Session-Id: abc) -> server (must own session abc)</span></span>
<span data-line=""><span>client -> tools/call (Mcp-Session-Id: abc) -> same server instance</span></span>
<span data-line=""> </span>
<span data-line=""><span>After (2026-07-28):</span></span>
<span data-line=""><span>client -> tools/list -> load balancer -> server A (returns list + ttlMs)</span></span>
<span data-line=""><span>client -> tools/call -> load balancer -> server B (different instance, same result)</span></span></code></pre></figure>
<p>You no longer need the same physical instance to handle both calls. That is the entire ballgame. In practice it means three things for your infrastructure: you can drop sticky session config from your load balancer, you can remove any shared Redis-based session store, and you can stop sending Mcp-Session-Id from your gateway routing rules.</p>
<p>The new Mcp-Method header is the small detail that pays off for gateway authors. Instead of parsing the JSON-RPC body to know whether a request is tools/list or tools/call, the gateway can read a single header and route by method. That lets you split tools/list (cacheable, idempotent) onto edge nodes and tools/call (mutating, sometimes expensive) onto warm origin nodes.</p>
<h2 id="what-do-extensions-tasks-and-mcp-apps-add">What do Extensions, Tasks, and MCP Apps add?</h2>
<p>The 2026-07-28 spec formalizes Extensions as the way new capabilities ship outside the core protocol. The MCP team calls out Extensions as a deliberate move to keep the core small while still letting the ecosystem experiment. New capabilities propose themselves as Extensions first, prove production value, and only graduate into the base spec on the next date-stamped revision.</p>
<p>Two extensions matter most for new servers right now. The Tasks extension is built for long-running operations that need retries, expiry, and lifecycle tracking. If you have ever tried to wrap a 3-minute search index build inside a single MCP tool call and watched the client time out, Tasks fix that. The roadmap explicitly mentions "Tasks lifecycle refinement (retries, expiry policies)" as a priority for 2026.</p>
<p>The Tasks model looks roughly like this. Your tool call returns a task handle instead of a result. The client polls or subscribes to the task by id. The server keeps the task's state in whatever store you like (Redis, Postgres, S3), and the client decides how long it is willing to wait. The client and server never need to be in the same process for this to work. A different server instance can resume a task from its persistent state because the task id is the carrier, not a session.</p>
<p>MCP Apps are the second new extension. They let a server return server-rendered UI components instead of plain text or JSON payloads. The idea is that an MCP server for, say, a payments API can return a real card UI that the host renders inline. This is the closest MCP has come to giving servers presentation control, and it pulls the protocol closer to where Anthropic's Claude Skills and Claude Apps already live.</p>
<p>Both Extensions and Tasks shipped first as experimental features so the team could collect real-world feedback before locking them into a final spec. Treat them as production-ready in the RC window, but expect minor field renames before July 28.</p>
<h2 id="what-changes-in-authorization-and-error-handling">What changes in authorization and error handling?</h2>
<p>The new spec aligns authorization more closely with OAuth and OpenID Connect. The earlier spec left auth largely as an implementation detail, which meant every enterprise MCP deployment built its own bearer-token flow. The 2026-07-28 spec defines how OAuth flows interact with MCP transport so a single enterprise SSO setup covers all your MCP servers.</p>
<p>The roadmap lists "Governance Maturation" and "Enterprise Readiness" as priority areas, with audit trails, SSO auth, and gateway behavior as concrete deliverables. If you have been blocked on MCP rollout because security teams could not approve a custom auth flow, this is the change that unblocks you.</p>
<p>Error handling also tightens up. The error code for missing resources shifts from the proprietary -32002 to the JSON-RPC standard -32602. Looks like a small change. In practice it means any client that hard-coded -32002 in retry logic now silently swallows the error and retries forever on legitimate not-found responses. I have already seen this fail in a private SDK that hardcoded the old code. Catch both for the next year of mixed deployments.</p>
<p>A nice secondary effect is that monitoring tools that already understand JSON-RPC standard codes (and there are a lot of them) suddenly understand MCP traffic for free. You lose a small amount of MCP-specific signal in exchange for free integration with every JSON-RPC tracing tool out there. That is a good trade.</p>
<h2 id="which-existing-code-breaks-when-you-upgrade">Which existing code breaks when you upgrade?</h2>
<p>The breaking surface is small in lines of code but wide in impact. Here is what I am tracking across my own servers.</p>
<p>Anywhere your code asserts on Mcp-Session-Id will break. The header is no longer guaranteed and any session-based routing logic needs to come out of your gateway and your client. If you are running NGINX or an Envoy sidecar with rules on Mcp-Session-Id, those rules now do nothing. Worse, they may silently route ALL traffic to one origin because the default fallthrough rule kicks in.</p>
<p>Anywhere your client uses session state implicitly via the handshake will break. The new spec asks you to thread identifiers explicitly between tool calls. If you call tools/list and store the result keyed by session, you need to switch to keying by server URL plus ttlMs.</p>
<p>Anywhere your retry logic catches -32002 will silently misbehave. Change it to -32602 or, better, catch both for the next year of mixed deployments while clients catch up.</p>
<p>Anywhere your gateway does deep packet inspection on the body to route requests will break in a more positive direction. You can rip that logic out and rely on the new Mcp-Method header for routing decisions. Code you delete is code that cannot break in production.</p>
<p>If you are using a Tier 1 SDK (the Anthropic-maintained Python and TypeScript SDKs), the SDK will hide most of these changes from you within the ten-week validation window. If you are on a community SDK, check whether your maintainer is in the Tier 1 list before you plan a migration date. The SDK tier system is itself new to MCP and tells you which SDKs the spec maintainers actively coordinate with on breaking changes.</p>
<h2 id="should-you-migrate-before-july-28-or-after">Should you migrate before July 28 or after?</h2>
<p>It depends on whether you control both the client and the server. If you do, migrate as soon as the SDK you depend on ships RC support, which for Tier 1 should land within four to six weeks. The new protocol is friendlier to the production patterns you already want anyway.</p>
<p>If you ship a public MCP server consumed by clients you do not control, hold until July 28 and ship support for both spec versions on the same endpoint for at least a quarter. The spec's deprecation policy and the SDK tier system make dual support cheaper than it sounds, since both protocols can share the same handler code with a thin compatibility layer that reads the request and returns either a session-id (for old clients) or no session (for new ones).</p>
<p>The one case where you should rush is if you are running MCP in a Kubernetes deployment behind an L7 load balancer. The current setup probably has at least one band-aid (sticky sessions, Redis session store, deep packet inspection) that exists only because of the stateful protocol. Migrating lets you delete that code, and code you delete is code that cannot break in production.</p>
<p>There is also a clear case where you should wait. If your MCP server depends on a community SDK that is not in the Tier 1 list, you do not have a guarantee that the SDK will ship 2026-07-28 support before July 28 itself. Plan a quarter of slack. Do not promise a migration date until the SDK author publishes their own.</p>
<h2 id="what-does-this-mean-for-your-mcp-servers">What does this mean for your MCP servers?</h2>
<p>The 2026-07-28 spec is a clean win for anyone running MCP in production. The stateless core removes the awkward fit between MCP and modern serverless or horizontally scaled deployments, and the Tasks and MCP Apps extensions plug gaps that every real deployment has been working around with custom code.</p>
<p>The bigger lesson, though, is that MCP is starting to act like an open protocol with real production users, not a research experiment from Anthropic. Date-stamped versioned specs, an SEP process, formal SDK tiers, and a published roadmap with priority areas all push it in the direction of HTTP or LSP, not a vendor SDK. That is a healthy sign even if the immediate migration is annoying.</p>
<p>If you are starting a new MCP server this week, target the 2026-07-28 RC directly. If you have an existing one, start with deleting your Mcp-Session-Id routing rules and your shared session store. Most teams will find that the migration shrinks their MCP stack rather than grows it, which is the right direction for a protocol that is supposed to be the lowest-friction way to give an LLM access to tools.</p>
<p>For more on the new spec, see the official <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/">2026-07-28 Release Candidate announcement</a>, the <a href="https://blog.modelcontextprotocol.io/posts/2026-mcp-roadmap/">2026 MCP Roadmap</a>, and the current <a href="https://modelcontextprotocol.io/specification/2025-11-25">2025-11-25 specification</a> for the baseline you are migrating from.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/build-stateless-mcp-server-2026-07-28-spec">How to Build a Stateless MCP Server for the 2026-07-28 Spec</a>. The applied tutorial that ships with code once you understand the protocol shift this post covers.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-ai-2-mcp-annotations-tutorial">Spring AI 2.0 MCP Annotations: From Tool to Production</a>. How the Spring AI 2.0 abstractions sit on top of MCP and what they hide from you.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects: Which One Should You Use?</a>. When to pick MCP at all, given the other ways Claude exposes capabilities to agents.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[TanStack npm Supply Chain Attack 2026: How OIDC Beat SLSA]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026</link>
      <guid>https://www.rabinarayanpatra.com/blogs/tanstack-supply-chain-attack-2026</guid>
      <pubDate>Fri, 22 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[On May 11, 2026, attackers hijacked TanStack's release pipeline mid-build to publish 84 malicious npm packages with valid SLSA provenance. Here's how.]]></description>
      <content:encoded><![CDATA[<p>At 19:20 UTC on May 11, 2026, npm received the first batch of 42 malicious package versions from the official <code>@tanstack</code> namespace. Six minutes later, the second batch landed. Twenty minutes after that, an external researcher posted a full technical analysis to the TanStack repo. By the time most of Europe woke up, 84 versions across 42 packages were already deprecated.</p>
<p>The packages were not published by an attacker who stole credentials. They were published by TanStack's legitimate release pipeline, signed with valid SLSA Build Level 3 provenance, attested through Sigstore, and pushed using a freshly minted OIDC token. Every supply-chain control we have been recommending for the past three years worked exactly as designed. And it did not matter.</p>
<p>I run my own published package on Maven Central (<code>sanitizer-lib</code>) and I use TanStack Router in client projects. So this one stings personally. Let me walk you through what actually happened, why our defenses missed it, and what every CI maintainer needs to change before the next round.</p>
<h2 id="what-happened-in-the-tanstack-supply-chain-attack">What happened in the TanStack supply chain attack?</h2>
<p>The TanStack supply chain attack is a multi-stage compromise that ran the malicious code through TanStack's own legitimate release infrastructure to publish 84 backdoored npm package versions across 42 packages in the <code>@tanstack/*</code> namespace. It then wormed into more than 170 downstream packages including <code>@mistralai/mistralai</code> (2.2.2 through 2.2.4), 40+ packages in the UiPath namespace, and 19 aviation data packages in <code>@squawk</code>.</p>
<p>For scale: <code>@tanstack/react-router</code> alone pulls roughly 12.7 million weekly downloads. If you are doing modern React routing in 2026, there is a decent chance you sat in the blast radius.</p>
<p>The incident is being tracked as CVE-2026-45321 and GHSA-g7cv-rxg3-hmpx. The threat group is TeamPCP, also known as DeadCatx3, PCPcat, ShellForce, and CipherForce. This is the same crew that compromised Aqua Security's Trivy scanner in March 2026 and the Bitwarden CLI npm package in April. Researchers at Wiz, Snyk, and StepSecurity collectively named this family "Mini Shai-Hulud" because it borrows the worm-like republish pattern from the original September 2025 Shai-Hulud incident.</p>
<p>Here is the compressed timeline.</p>
<table>
<thead>
<tr>
<th>Time (UTC)</th>
<th>What happened</th>
</tr>
</thead>
<tbody>
<tr>
<td>May 10, 17:16</td>
<td>Attacker creates the fork <code>github.com/zblgg/configuration</code></td>
</tr>
<tr>
<td>May 10, 23:29</td>
<td>Malicious commit <code>65bf499d</code> lands, authored as <code>claude &#x3C;claude@users.noreply.github.com></code> to look legitimate</td>
</tr>
<tr>
<td>May 11, 10:49</td>
<td>PR #7378 opened against <code>TanStack/router</code></td>
</tr>
<tr>
<td>May 11, 11:11 to 11:29</td>
<td>Force-pushes trigger <code>pull_request_target</code> workflows, GitHub Actions cache gets poisoned</td>
</tr>
<tr>
<td>May 11, 11:31</td>
<td>PR closed, fork branch deleted (cleanup)</td>
</tr>
<tr>
<td>May 11, 19:15</td>
<td>Release workflow re-runs against main, restores the poisoned cache</td>
</tr>
<tr>
<td>May 11, 19:16</td>
<td>Fresh release triggered by merge of PR #7382</td>
</tr>
<tr>
<td>May 11, 19:20</td>
<td>npm receives first batch of 42 malicious versions</td>
</tr>
<tr>
<td>May 11, 19:26</td>
<td>npm receives second batch of 42 versions</td>
</tr>
<tr>
<td>May 11, 19:46</td>
<td>StepSecurity researcher ashishkurmi opens issue #7383 with full IOCs</td>
</tr>
<tr>
<td>May 11, 20:19</td>
<td>TanStack begins deprecation</td>
</tr>
<tr>
<td>May 11, 21:03</td>
<td>All 84 versions deprecated</td>
</tr>
<tr>
<td>May 11, 22:13 to 23:55</td>
<td>npm removes tarballs registry-side</td>
</tr>
<tr>
<td>May 12, 05:02</td>
<td>Formal IOC list sent to npm and GitHub Security</td>
</tr>
</tbody>
</table>
<p>Notice something. The fork and the malicious commit were prepared a full day before the payload went live. The release was not a spontaneous event. It was held in the cache, dormant, waiting for the next legitimate release workflow run.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/tanstack-supply-chain-attack-2026-timeline.webp" alt="Timeline of the TanStack supply chain attack on May 11, 2026" width="1600" height="905"></p>
<h2 id="how-did-the-attacker-chain-three-github-actions-vulnerabilities">How did the attacker chain three GitHub Actions vulnerabilities?</h2>
<p>The attack works by stacking three flaws that are each well-documented in isolation. Combined, they let attacker code execute inside the release workflow's trust boundary without ever touching the production branch.</p>
<h3 id="the-first-link-pull_request_target-abuse">The first link: pull_request_target abuse</h3>
<p>GitHub's <code>pull_request_target</code> event trigger runs workflows in the base repository's context, not the fork's context. That is by design. It exists so that workflows can apply labels, post comments, or check size budgets on incoming PRs without requiring maintainer approval. The trade-off is that any code those workflows execute has access to the base repo's secrets.</p>
<p>The TanStack <code>bundle-size.yml</code> workflow used <code>pull_request_target</code> and then executed fork-controlled code inside it. This pattern is so common and so dangerous that it has a name: the "Pwn Request" pattern. The attacker did not have to do anything clever here. They submitted a PR that swapped out a setup script with a malicious version, and the workflow happily ran it with full repository privileges.</p>
<p>If you maintain any open-source repository on GitHub, run this right now:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -rE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">pull_request_target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .github/workflows/</span></span></code></pre></figure>
<p>Then audit each match. The rule is simple: either do not check out fork code in <code>pull_request_target</code> workflows, or check it out into an isolated job that has no access to secrets and no permission to write to the cache.</p>
<h3 id="the-second-link-github-actions-cache-poisoning">The second link: GitHub Actions cache poisoning</h3>
<p>This is the cleverest part of the chain. The attacker did not need to keep their code in the repo. They needed to leave it in the cache.</p>
<p>GitHub Actions caches are scoped by branch by default, but pull requests inherit access to base-branch caches. The cache key is deterministic. For pnpm, it looks like <code>Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}</code>. The attacker pre-computed the exact key that the legitimate <code>release.yml</code> workflow would use after merging a future PR. That key was:</p>
<pre><code>Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
</code></pre>
<p>The malicious <code>vite_setup.mjs</code> in the attacker's PR wrote a tampered pnpm store under that key. When the PR was closed, the cache entry stayed. It sat there for hours. When a legitimate release workflow later ran on <code>main</code>, it restored that cache as part of its normal install step. The poisoned store contained the malicious binaries the attacker needed for the next stage.</p>
<p>A critical detail from the TanStack postmortem: cache writes use a runner-internal token, not the workflow's <code>GITHUB_TOKEN</code>. That means standard permission boundaries do not apply. Restricting <code>GITHUB_TOKEN</code> to read-only does nothing for this attack vector.</p>
<h3 id="the-third-link-oidc-token-theft-from-runner-memory">The third link: OIDC token theft from runner memory</h3>
<p>This is the part that crossed every line I thought we still had.</p>
<p>When a GitHub Actions runner needs to publish to npm using trusted publishing, it requests a short-lived OIDC token from GitHub. That token is minted in memory, used to authenticate the publish, and then discarded. It is never written to disk. It never appears in environment variables that a child process can read. The runner generates it, uses it, throws it away.</p>
<p>Or so we thought.</p>
<p>The attacker's binaries, restored from the poisoned cache, did something that the original tj-actions compromise in March 2025 first demonstrated at scale. They read <code>/proc/&#x3C;pid>/maps</code> to find the memory regions of the <code>Runner.Worker</code> process. Then they read <code>/proc/&#x3C;pid>/mem</code> directly to scrape the OIDC token out of process memory while the runner was alive and authenticated.</p>
<p>Once they had the token, they did not need any other credential. They sent direct HTTPS POST requests to <code>registry.npmjs.org</code> from the runner, authenticated as TanStack's release pipeline, with a valid SLSA attestation generated by Sigstore as a normal byproduct of the OIDC flow. The result was 84 malicious package versions that look indistinguishable from legitimate releases, because in every technical sense they were legitimate releases. The pipeline that built and published them was the real one. The code it pulled out of the cache was not.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/tanstack-supply-chain-attack-2026-attack-chain.webp" alt="TanStack attack chain showing pull_request_target abuse, cache poisoning, and OIDC token extraction" width="1600" height="905"></p>
<h2 id="why-did-slsa-provenance-fail-to-protect-downstream-consumers">Why did SLSA provenance fail to protect downstream consumers?</h2>
<p>Here is the part that broke my brain for a few hours.</p>
<p>SLSA Build Level 3 is the gold standard we have been telling teams to demand from upstream dependencies. The attestations on these 84 packages were valid. Sigstore verified the build process correctly. The provenance chain pointed to the actual TanStack repository, the actual release workflow run, and the actual GitHub Actions runner. Every signature checked out.</p>
<p>So what did the provenance actually attest to?</p>
<p>It attested that the build ran inside GitHub Actions, from the canonical TanStack/router repository, in a workflow defined in that repo, using a runner with a verifiable identity. All of that was true. The attestation does not, and cannot, attest that the code the build pipeline executed was the code in the repository. It only attests to the pipeline.</p>
<p>In normal operation, those two are the same thing. The pipeline checks out the repo, the repo is the code, the code gets built. But when an attacker can inject code into the pipeline through a side channel (in this case, the cache), the pipeline is no longer building what the repo contains. It is building a hybrid. SLSA says nothing about the inputs the pipeline pulled from outside the source tree.</p>
<p>This is the lesson I want every AppSec team to take from this incident. Provenance answers the question "was this built from the canonical repo in a controlled environment?" It does not answer "is the code in this package safe?" Those used to be the same question for practical purposes. They are no longer.</p>
<p>What does this mean operationally? You still want SLSA. You still want OIDC trusted publishing. You still want Sigstore. But you need behavioral analysis at install time as a complementary control. Sandboxed installs that inspect what <code>postinstall</code> scripts actually do, what network connections they make, what files they touch. Static and dynamic analysis of the published artifact, not just verification of its origin. I covered this exact pattern in my earlier post on <a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-driven anomaly detection for security</a>, and the TanStack incident is the strongest argument for it I have seen.</p>
<h2 id="how-does-the-gh-token-monitor-dead-man-switch-work">How does the gh-token-monitor dead-man switch work?</h2>
<p>The malicious payload installed on infected machines is roughly 2.3 MB after the obfuscation layers come off. The file is <code>router_init.js</code> and its SHA256 is <code>ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c</code>. It uses three obfuscation layers stacked on top of each other.</p>
<p>The first layer is JavaScript Obfuscator string-array rotation, the kind of thing you find in any commercial obfuscation product. The second is a Fisher-Yates substitution cipher seeded by PBKDF2-SHA256 with the salt <code>svksjrhjkcejg</code>. The third is eleven AES-256-GCM encrypted payloads that require the Bun runtime to decrypt, which is why the malware installs Bun on the target machine if it is not already present.</p>
<p>Once everything decrypts, the payload harvests credentials. The scope is brutal. AWS IMDSv2 metadata endpoints, <a href="https://www.rabinarayanpatra.com/snippets/aws-cli/secrets-manager-get">AWS Secrets Manager</a> keys, GCP metadata server tokens, Azure managed identity tokens, Kubernetes service account tokens, HashiCorp Vault tokens, GitHub Actions secrets, GitLab CI tokens, CircleCI tokens, <code>.npmrc</code> files, SSH private keys, and (worth calling out because it is new) Claude Code session history files from <code>.claude/projects/*.jsonl</code>. That last one tells me the threat actor is paying attention to where developers actually keep their secrets in 2026, not where the OWASP guides from 2018 said they would be.</p>
<p>But the part that should make you nervous is what happens after the credentials are out the door. The malware installs a persistence daemon called <code>gh-token-monitor</code>.</p>
<p>On macOS, the daemon registers as a LaunchAgent at:</p>
<pre><code>~/Library/LaunchAgents/com.user.gh-token-monitor.plist
</code></pre>
<p>On Linux, it registers as a systemd user service at:</p>
<pre><code>~/.config/systemd/user/gh-token-monitor.service
</code></pre>
<p>The daemon polls GitHub every 60 seconds, checking whether the stolen credentials still work. If it detects that the GitHub token has been revoked (the obvious first response from any incident-response team), it triggers a destructive payload. On the affected machines, that payload was an attempt to run <code>rm -rf ~/</code> against the user's home directory.</p>
<p>The good news is that on modern Linux distributions, <code>rm</code> refuses to operate on <code>/</code> or <code>~/</code> without the <code>--no-preserve-root</code> flag, so the actual destruction is limited in many cases. But "many" is not "all", and you do not want to find out which version of <code>rm</code> your laptop is shipping when you are already mid-incident.</p>
<p>The implication is operational. If you suspect a compromise, do not rotate the GitHub token first. Find and remove the <code>gh-token-monitor</code> daemon first, then rotate. This is the opposite of every incident-response playbook I have ever seen.</p>
<p>The malware also drops persistence hooks into editor config directories. On the systems Wiz analyzed, it wrote <code>router_runtime.js</code> into <code>.claude/</code> and added entries to <code>.claude/settings.json</code>, and dropped <code>setup.mjs</code> plus a malicious task entry in <code>.vscode/tasks.json</code>. The point is that even after you remove the daemon and rotate credentials, the next time you open the project in your editor, the hooks can reinstall everything. Audit those directories explicitly.</p>
<h2 id="what-should-you-do-if-you-installed-an-affected-version">What should you do if you installed an affected version?</h2>
<p>Order matters here. If you installed an affected version, follow these steps in this exact sequence.</p>
<p>First, find the daemon. Run these checks on every developer machine and CI runner that touched the compromised packages between May 11 and your last clean restore.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># macOS</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">launchctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gh-token-monitor</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -la</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/Library/LaunchAgents/com.user.gh-token-monitor.plist</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Linux</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> list-units</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gh-token-monitor</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -la</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/.config/systemd/user/gh-token-monitor.service</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Either OS</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">find</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">router_init.js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">find</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">router_runtime.js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span></span></code></pre></figure>
<p>Second, kill the daemon before doing anything else.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># macOS</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">launchctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> unload</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/Library/LaunchAgents/com.user.gh-token-monitor.plist</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">rm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/Library/LaunchAgents/com.user.gh-token-monitor.plist</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Linux</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> stop</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gh-token-monitor</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> disable</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> gh-token-monitor</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">rm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/.config/systemd/user/gh-token-monitor.service</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">systemctl</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --user</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> daemon-reload</span></span></code></pre></figure>
<p>Third, remove the editor hooks.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Check what was added</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">cat</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .claude/settings.json</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">cat</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> .vscode/tasks.json</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Remove the malicious entries by hand. Do not run a script you do not understand.</span></span></code></pre></figure>
<p>Fourth, audit your lockfile. Search for any of the 84 affected versions. The headline versions to check first are <code>@tanstack/react-router@1.169.5</code> and <code>@1.169.8</code>, <code>@tanstack/vue-router@1.169.5</code> and <code>@1.169.8</code>, and <code>@tanstack/solid-router@1.169.5</code> and <code>@1.169.8</code>. The full list lives in the official advisory.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@tanstack/.*1\.169\.(5|8)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> package-lock.json</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pnpm-lock.yaml</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> yarn.lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span></span></code></pre></figure>
<p>Fifth, rotate every credential the malware could have touched. That means npm tokens, GitHub personal access tokens, GitHub Actions OIDC trust configurations, AWS access keys, Vault tokens, Kubernetes service account tokens, SSH keys, and any provider-specific secrets stored in <code>~/.aws</code>, <code>~/.gcloud</code>, <code>~/.kube</code>, or <code>~/.npmrc</code>.</p>
<p>Sixth, block the C2 infrastructure at your DNS or proxy layer.</p>
<pre><code>git-tanstack.com         # typosquat
filev2.getsession.org    # Session messenger exfil
seed1.getsession.org
seed2.getsession.org
seed3.getsession.org
83.142.209.194           # primary C2 IP
</code></pre>
<p>Seventh, scan for the campaign string <code>EveryBoiWeBuildIsAWormyBoi</code> in any process memory, log file, or repository content. It is the easiest reliable IOC the malware leaves behind.</p>
<p>If you handled the <a href="https://www.rabinarayanpatra.com/blogs/axios-npm-supply-chain-attack-2026">Axios npm compromise from March 2026</a>, most of this routine will feel familiar. The key difference is the order: there, you could rotate credentials first because the malware did not retaliate. Here, retaliation is the whole point of the persistence layer.</p>
<h2 id="how-do-you-harden-your-own-github-actions-workflows-against-this">How do you harden your own GitHub Actions workflows against this?</h2>
<p>Even if you never touched a TanStack package, the techniques used in this attack apply to any repository that uses <code>pull_request_target</code> and any organization that publishes to npm via OIDC trusted publishing. Which, increasingly, is every JavaScript shop in production.</p>
<p>Here is what I have changed in my own workflows since reading the TanStack postmortem.</p>
<p><strong>Audit every <code>pull_request_target</code> workflow.</strong> For each one, ask: does this workflow execute fork-controlled code? If yes, either remove the fork-code execution or split the workflow into two: an untrusted "read fork code" job that runs without any secrets, and a trusted "act on results" job that runs separately. Pass data between them only as strict, validated outputs.</p>
<p><strong>Guard against fork pushes.</strong> Add an early job that bails out if the PR is from a non-trusted source, before any setup steps run.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">jobs</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  guard</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    runs-on</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ubuntu-latest</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    steps</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      -</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Block fork PRs from privileged workflow</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> github.event.pull_request.head.repo.full_name != github.repository</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> |</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">          echo "Refusing to run privileged workflow on fork PR"</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">          exit 1</span></span></code></pre></figure>
<p><strong>Pin every third-party action to a commit SHA.</strong> Tags can be moved. SHAs cannot.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Don't do this</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">-</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> uses</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> actions/checkout@v4</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Do this</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">-</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> uses</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> # v4.1.7</span></span></code></pre></figure>
<p><strong>Separate cache namespaces.</strong> Use different cache keys for PR workflows and release workflows so a poisoned PR cache cannot be restored by a release run.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">-</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> uses</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> # v4.0.2</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  with</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~/.pnpm-store</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ${{ github.workflow }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}</span></span></code></pre></figure>
<p>That <code>${{ github.workflow }}</code> prefix means PR workflows and release workflows have disjoint cache namespaces.</p>
<p><strong>Require manual approval on publish.</strong> OIDC trusted publishing is excellent, but the lack of per-publish human review is the weakest point in the chain. Configure your release workflow as an environment with required reviewers, even for trusted maintainers. A 30-second approval check would have stopped this entire attack.</p>
<p><strong>Reduce the maintainer surface.</strong> Every additional human with publish rights is a credential-theft target. Trim the publisher list on your npm scope to the smallest number that keeps the project healthy. The TanStack postmortem flags this as one of their own action items.</p>
<p><strong>Enable a release-age cooldown.</strong> Set <code>min-release-age</code> in your <code>.npmrc</code> to delay adopting newly published versions. Seven days is the common recommendation. It does not protect you from a slow-burn attack, but it gives the ecosystem time to catch a fast-detonating one.</p>
<pre><code># .npmrc
min-release-age=10080  # 7 days in minutes
</code></pre>
<p>If you run a microservices environment, treat your CI runners with the same zero-trust posture you apply to production workloads. The pattern I described in my <a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">zero-trust microservices post</a> applies just as much to GitHub Actions as it does to Kubernetes.</p>
<h2 id="what-does-this-mean-for-the-future-of-npm-trust">What does this mean for the future of npm trust?</h2>
<p>Here is the part that worries me.</p>
<p>For three years, we have been telling teams that the answer to supply chain attacks is provenance. Sign your packages. Publish via OIDC. Demand SLSA Build Level 3. The TanStack incident is the first one where every single one of those defenses worked, and the attack still succeeded.</p>
<p>This is not a reason to abandon any of them. They still raise the cost for attackers and they still help in the median case, which is credential theft. But they are no longer sufficient on their own, and anyone telling you otherwise is selling something.</p>
<p>What I think changes from here:</p>
<p>Cache infrastructure is now part of the trusted compute base. Treat GitHub Actions caches as input that needs the same scrutiny as source code. Cache poisoning is not a theoretical concern anymore; it is a documented attack pattern with a real CVE attached to it.</p>
<p>Runtime install-time analysis matures from optional to required. Tools like Socket, Snyk, and StepSecurity that watch what packages do during installation are no longer "nice to have" for security-conscious shops. They are the only practical defense against this class of attack, because they evaluate the behavior of the artifact, not its origin.</p>
<p>The npm registry's "no unpublish if dependents exist" policy needs revisiting. In the TanStack incident, that policy delayed full tarball removal by hours. For verified maintainer-initiated incident response, there needs to be a faster path. The postmortem flags this explicitly.</p>
<p>OIDC trusted publishing needs per-publish review. The current model authenticates the pipeline, not the publish event. A 30-second human approval step before a token is minted would have stopped this attack cold. I expect to see this as a configurable option within the next 12 months.</p>
<p>The TanStack team's response to this was, by every measure I can find, excellent. Detection within 26 minutes. Full deprecation in under two hours. Public disclosure on the same day. They did everything right. And the attack still landed 84 malicious package versions onto npm with valid provenance.</p>
<p>That is the message. The defenders are not slow. The attackers are not lazy. We are watching a category of attack that operates inside our trust boundaries, and the answer is not better trust boundaries. It is treating every artifact as untrusted until proven safe, every time.</p>
<p>For more on this campaign, see the <a href="https://tanstack.com/blog/npm-supply-chain-compromise-postmortem">TanStack official postmortem</a>, the <a href="https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised">Wiz Mini Shai-Hulud analysis</a>, the <a href="https://snyk.io/blog/tanstack-npm-packages-compromised/">Snyk technical breakdown</a>, the <a href="https://github.com/advisories/GHSA-g7cv-rxg3-hmpx">official GitHub Security Advisory GHSA-g7cv-rxg3-hmpx</a>, and the <a href="https://www.stepsecurity.io/blog/node-ipc-npm-supply-chain-attack">related StepSecurity report on node-ipc</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/axios-npm-supply-chain-attack-2026">The Axios npm Hack: How North Korea Hijacked 100M Weekly Downloads</a>: the March 2026 incident that set the stage for the OIDC-vs-SLSA conversation.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-Driven Anomaly Detection for Security</a>: behavioral analysis at install time is the only practical defense against pipeline-hijack attacks.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero-Trust Microservices with Spring Security</a>: apply the same zero-trust posture to CI runners that you apply to production services.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[PostgreSQL 18 Temporal Foreign Keys with Spring Boot JPA]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/postgresql-18-temporal-foreign-keys-spring-boot</link>
      <guid>https://www.rabinarayanpatra.com/blogs/postgresql-18-temporal-foreign-keys-spring-boot</guid>
      <pubDate>Thu, 21 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[PostgreSQL 18 temporal foreign keys with Spring Boot: WITHOUT OVERLAPS, PERIOD clause, Hibernate daterange mapping, and the ON DELETE gotcha nobody mentions.]]></description>
      <content:encoded><![CDATA[<p>PostgreSQL 18 shipped temporal foreign keys. The kind of feature SQL standards committees promised back in SQL:2011 and most database vendors quietly ignored. Now it's in mainline PG and the Java ecosystem has almost no tutorials on how to use it from Spring Boot.</p>
<p>I went looking for "Spring Boot temporal foreign key" guides last week. The top results were 2018 Baeldung posts on hand-rolled <code>validFrom/validTo</code> columns with zero database-level constraints. Plenty of <code>BETWEEN</code> queries. Plenty of "remember to add an index". No one talks about PG18 yet. So I built a working example, hit every gotcha, and wrote it up.</p>
<p>This post walks the full path: what temporal FKs actually solve, the PostgreSQL 18 syntax, how to map range columns in Hibernate, the Spring Data repository patterns that work, and the ON DELETE behavior that will absolutely surprise you if you skip the docs.</p>
<h2 id="what-problem-do-temporal-foreign-keys-solve">What problem do temporal foreign keys solve?</h2>
<p>A temporal foreign key enforces that a child row's time range fits entirely inside its parent row's time range, at the database level, on every insert and update. That's the part regular foreign keys can't do.</p>
<p>Take a classic example. An employee changes departments three times in five years. A project assignment references the employee. The business rule is: a project assignment can only exist during a period when the employee was actually employed.</p>
<p>The hand-rolled approach looks like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> TABLE</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> employees</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    emp_id </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">BIGINT</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    valid_from </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    valid_to </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DATE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    PRIMARY KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (emp_id, valid_from)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> TABLE</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> project_assignments</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    assignment_id </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">BIGINT</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> PRIMARY KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    emp_id </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">BIGINT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    assignment_start </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    assignment_end </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DATE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    FOREIGN KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (emp_id) </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">REFERENCES</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> employees(emp_id)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>Every Spring shop I've worked at had a variation of this. And every one of them had bugs. Project assignments referencing employee IDs whose valid period ended six months ago. Manual <code>BETWEEN</code> checks in service layers that someone forgot to update. Audit failures during quarterly reviews. The data model lied about what it was.</p>
<p>PG18 fixes this at the constraint level. The constraint is the truth, not a service-layer hope.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/postgresql-18-temporal-foreign-keys-spring-boot-diagram.webp" alt="PostgreSQL 18 temporal foreign keys connecting employees with overlapping validity periods to project assignments" width="1600" height="905"></p>
<h2 id="how-does-postgresql-18-implement-without-overlaps-and-period">How does PostgreSQL 18 implement WITHOUT OVERLAPS and PERIOD?</h2>
<p>PG18 introduces two new clauses: <code>WITHOUT OVERLAPS</code> for primary and unique keys, and <code>PERIOD</code> for foreign keys. Both rely on range types and GiST indexes under the hood.</p>
<p>Here's the employees table redone the right way:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> EXTENSION </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">IF</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> EXISTS</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> btree_gist;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> TABLE</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> employees</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    emp_id      </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">BIGINT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">    name</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        VARCHAR</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    department  </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">VARCHAR</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">50</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    salary      </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NUMERIC</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">10</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    valid_period daterange </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    PRIMARY KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (emp_id, valid_period </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">WITHOUT</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> OVERLAPS)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>A few things worth pointing out.</p>
<p>The <code>btree_gist</code> extension is required because the primary key mixes a regular <code>BIGINT</code> column with a range column. PG needs a GiST index that can handle both, and <code>btree_gist</code> provides the btree operator support inside GiST. If you forget it, the <code>CREATE TABLE</code> will fail with a confusing operator error.</p>
<p>The <code>valid_period</code> column uses <code>daterange</code>, one of PostgreSQL's built-in range types. You can also use <code>tstzrange</code> for timestamp ranges or <code>int4range</code> for numeric ranges. The constraint creates a GiST index automatically. Try to insert two rows for the same <code>emp_id</code> with overlapping <code>valid_period</code> values and PG rejects it.</p>
<p>Now the project assignments table with a real temporal FK:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> TABLE</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> project_assignments</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    assignment_id    </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">BIGSERIAL</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> PRIMARY KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    emp_id           </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">BIGINT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    project_name     </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">VARCHAR</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    assignment_period daterange </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    FOREIGN KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (emp_id, </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">PERIOD</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> assignment_period)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        REFERENCES</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> employees (emp_id, </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">PERIOD</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> valid_period)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>The <code>PERIOD</code> keyword tells PG that the last column is a range. The check is not equality. The check is containment: the parent's matching rows must, in combination, fully cover the child's range. If the employee's <code>valid_period</code> ends June 1 2024 and the project assignment runs March 1 to August 1 2024, the insert fails because June 1 to August 1 has no parent row covering it.</p>
<p>You can verify this with a deliberate bad insert:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">INSERT INTO</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> employees (emp_id, </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">name</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, department, salary, valid_period)</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">VALUES</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Alice</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Engineering</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">80000</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, daterange(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2024-01-01</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2024-06-01</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">INSERT INTO</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> project_assignments (emp_id, project_name, assignment_period)</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">VALUES</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Migration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, daterange(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2024-03-01</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2024-08-01</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">-- ERROR: insert or update on table "project_assignments" violates foreign key constraint</span></span></code></pre></figure>
<p>The constraint catches it. No service code needed.</p>
<h2 id="how-do-you-map-daterange-columns-in-hibernate">How do you map daterange columns in Hibernate?</h2>
<p>Hibernate 6 added native support for PostgreSQL range types through <code>PostgreSQLRangeJdbcType</code>, but the cleanest path for a Spring Boot app is still the <code>hypersistence-utils</code> library. It's the rebranded version of what most of us used as <code>hibernate-types-52</code> for years, maintained by Vlad Mihalcea.</p>
<p>Add the dependency to your <code>pom.xml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.hypersistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">hypersistence-utils-hibernate-63</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">3.10.7</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>If you're on Spring Boot 3.5+, you'll be on Hibernate 6.6, so use the <code>hypersistence-utils-hibernate-63</code> artifact. Older Spring Boot 3.x versions use <code>-62</code>. The version numbers track Hibernate ORM, not Spring Boot.</p>
<p>Now the entity:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> io</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hypersistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">utils</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> io</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hypersistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">utils</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PostgreSQLRangeType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> jakarta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">persistence</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">*</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">annotations</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">math</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">BigDecimal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Entity</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Table</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">employees</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">IdClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">EmployeeId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Employee</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Id</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">emp_id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Id</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">PostgreSQLRangeType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">valid_period</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> columnDefinition</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">daterange</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> validPeriod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> department</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Column</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">nullable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> BigDecimal</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> salary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // getters, setters, constructors</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>A few things to flag.</p>
<p>The <code>@IdClass(EmployeeId.class)</code> is needed because the primary key is composite. The <code>EmployeeId</code> class is a plain Java record or class with the two ID fields and <code>equals</code>/<code>hashCode</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> EmployeeId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> validPeriod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Serializable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span></code></pre></figure>
<p>The <code>@Type(PostgreSQLRangeType.class)</code> annotation is what makes Hibernate emit and read <code>daterange</code> correctly. The <code>columnDefinition = "daterange"</code> part is what makes JPA's schema generation produce the right column type if you let JPA create the schema. In production, you should use Flyway or Liquibase, not JPA schema generation, but it's worth being explicit either way.</p>
<p>You construct ranges like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> period </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">closedOpen</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2024</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2024</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p><code>closedOpen</code> matches PostgreSQL's <code>[)</code> notation, which is the most common range form for temporal data. Half-open intervals make adjacent ranges easy to reason about: <code>[Jan 1, Jun 1)</code> and <code>[Jun 1, Dec 1)</code> are adjacent, not overlapping.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/postgresql-18-temporal-foreign-keys-spring-boot-hibernate-mapping.webp" alt="Hibernate range type mapping: Java Range<LocalDate> through hypersistence-utils to PostgreSQL daterange" width="1600" height="905"></p>
<h2 id="how-do-you-write-the-entity-and-repository">How do you write the entity and repository?</h2>
<p>Spring Data JPA does most of the work, but you need a custom query for the overlap check at the application level. Database constraints catch invalid inserts, but the app still needs to ask "who was employed during this period?"</p>
<p>The repository:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> EmployeeRepository</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> JpaRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Employee</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> EmployeeId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> """</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">        SELECT *</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">        FROM employees</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">        WHERE emp_id = :empId</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">          AND valid_period &#x26;&#x26; :period</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        """</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> nativeQuery</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Employee</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findOverlapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Param</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Param</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">period</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> period</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The <code>&#x26;&#x26;</code> operator is PostgreSQL's range-overlap operator. You pass the range as a string in PG range literal form: <code>[2024-01-01,2024-06-01)</code>. JPA doesn't have a native range-binding mechanism, so the string approach is the pragmatic move. If you want strong typing, Vlad's library offers <code>Range.toString()</code> which formats correctly.</p>
<p>Here's a service method that finds the employee record valid for a given date:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> EmploymentService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> EmployeeRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> EmploymentService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">EmployeeRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">repo </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Employee</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> LocalDate</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> at</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> singleDay </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">format</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">[%s,%s]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> at</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> at</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findOverlapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> singleDay</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findFirst</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>For the project assignment side, the insert just works. Hibernate sends the daterange, PG checks the temporal FK, and if the assignment period isn't fully covered by some employee period, the transaction rolls back.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Transactional</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ProjectAssignment</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> assign</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> projectName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> period</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    ProjectAssignment</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pa </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ProjectAssignment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    pa</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setEmpId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">empId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    pa</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setProjectName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">projectName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    pa</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAssignmentPeriod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">period</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> assignmentRepo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pa</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>If the period extends past the employee's validity, you get a <code>DataIntegrityViolationException</code> wrapping the PG error. Catch it where your error handling needs it.</p>
<h2 id="what-are-the-gotchas-with-on-delete-and-temporal-foreign-keys">What are the gotchas with ON DELETE and temporal foreign keys?</h2>
<p>This is the one that will burn you if you skip the docs. PostgreSQL 18 does not support <code>CASCADE</code>, <code>RESTRICT</code>, <code>SET NULL</code>, or <code>SET DEFAULT</code> referential actions on temporal foreign keys. Only <code>NO ACTION</code> is allowed.</p>
<p>From the official PG18 CREATE TABLE docs: "In a temporal foreign key, this option is not supported." That sentence appears under every action except <code>NO ACTION</code>.</p>
<p>What does this mean in practice? If you delete an employee row that has dependent project assignments, PG raises a foreign key violation. You have to delete the assignments first, manually, in application code or in a database trigger you write yourself.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">-- This will fail at the second statement</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">DELETE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> FROM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> employees </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">WHERE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> emp_id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">-- ERROR: update or delete on table "employees" violates foreign key constraint</span></span></code></pre></figure>
<p>The workaround patterns I've seen:</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/postgresql-18-temporal-foreign-keys-spring-boot-ondelete-patterns.webp" alt="Three workaround patterns for ON DELETE on temporal foreign keys: app-level delete, soft delete, period close" width="1600" height="894"></p>
<p>The first is application-level deletion. Spring service methods that delete child rows before parent rows, wrapped in a <code>@Transactional</code> boundary. This is fine for small graphs, but it doesn't scale to deep hierarchies.</p>
<p>The second is soft delete on the parent. Add a <code>deleted_at</code> column, never actually delete rows, and let the temporal FK stay intact forever. Most enterprise systems do this anyway for audit reasons.</p>
<p>The third is to flip the model: instead of deleting the parent, you close the parent's period by updating <code>valid_period</code> to end at today's date. Future child inserts will fail. Existing child rows stay valid because their periods were covered when they were created.</p>
<p>The third approach is closest to the spirit of temporal modeling. You don't lose history. You just declare "this record stopped being valid at this point" and let the constraints enforce that going forward.</p>
<p>If you absolutely need cascading deletes, you can write a trigger that does the deletion manually before the parent delete fires. But at that point you're rebuilding what the standard wanted to give you, and the SQL spec authors decided cascade semantics on overlapping periods were too ambiguous to specify. They might be right.</p>
<h2 id="when-should-you-not-use-temporal-foreign-keys">When should you NOT use temporal foreign keys?</h2>
<p>Temporal foreign keys are powerful, and like most powerful features they're easy to overuse. I've seen teams reach for them in places where regular FKs would be simpler and just as correct.</p>
<p>Skip temporal FKs when:</p>
<p>You only need audit history. If the question is "what changed when?" and not "what was the state of the world on date X?", a separate audit log table with regular FKs is simpler. Frameworks like Hibernate Envers handle this well.</p>
<p>The child rows don't have an independent time dimension. If a project assignment is just "associated with employee 1 forever", you don't need temporal FKs. A regular FK with a <code>created_at</code> timestamp is fine.</p>
<p>You're modeling a single current state. CRUD apps where the latest row is the only thing that matters don't need temporal modeling. Add a <code>valid_period</code> column and you've created complexity you'll have to pay for in every query.</p>
<p>You can't pay the GiST index cost. GiST indexes are slower for point lookups than btree indexes. For high-throughput single-row reads, you'll feel it. Benchmark first.</p>
<p>When the model genuinely is temporal, the constraint-enforced version is a huge improvement over the hand-rolled version. The number of bugs you avoid by having PG check every insert is real. The cost of writing the queries against ranges instead of timestamps is real too, but it's a one-time cost. The bugs are forever.</p>
<h2 id="conclusion">Conclusion</h2>
<p>PostgreSQL 18 temporal foreign keys are one of the most under-discussed major-version features I've seen in a while. The Spring Boot ecosystem has barely caught up. If you're modeling employment, contracts, pricing tiers, policies, or anything else where state has duration, the new <code>WITHOUT OVERLAPS</code> and <code>PERIOD</code> clauses are worth learning even if you don't ship them tomorrow.</p>
<p>The piece I want more people to understand: this changes what your database can be the source of truth for. Before PG18, "this assignment must fall inside an employment period" was a service-layer concern that drifted out of sync. Now it's a constraint. The database tells the truth.</p>
<p>For more on the PG18 release, see the <a href="https://www.postgresql.org/docs/release/18.0/">PostgreSQL 18.0 release notes</a>, the <a href="https://neon.com/postgresql/postgresql-18/temporal-constraints">Neon writeup on temporal constraints</a>, and the <a href="https://www.postgresql.org/docs/current/sql-createtable.html">CREATE TABLE syntax docs</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide">Spring Boot Testcontainers Guide</a>. Real PG18 in tests beats mocked databases for catching constraint bugs early.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Hibernate Lazy Init Guide</a>. The other Hibernate gotcha that bites every Spring team.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide">PgBouncer Survival Guide</a>. Connection pooling matters more when GiST indexes are doing real work.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Spring Boot 4 AOT Data Repositories: The Underrated Feature]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/spring-boot-4-aot-data-repositories</link>
      <guid>https://www.rabinarayanpatra.com/blogs/spring-boot-4-aot-data-repositories</guid>
      <pubDate>Fri, 15 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Spring Boot 4's AOT data repositories generate query implementations at build time, cutting startup and unlocking GraalVM native image support.]]></description>
      <content:encoded><![CDATA[<p>I've used Spring Data for almost a decade. Repositories have always felt like the magical part of Spring Boot: write an interface with method names, get a working implementation at runtime through proxies and reflection. It works, but it's slow at startup, opaque when something breaks, and famously hostile to GraalVM native image compilation.</p>
<p>Spring Boot 4 changed that quietly. AOT Data Repositories generate the implementation at compile time. The repository proxy you got at runtime is now a real Java class on disk, written to source by the build, compiled with the rest of your code. Faster startup. Native images that work. Stack traces you can actually read.</p>
<p>I haven't seen many people talking about this, even though it's one of the more important changes in the Spring Boot 4 generation. This post covers what it does, how to turn it on, and what to know before you commit to it.</p>
<h2 id="what-are-spring-boot-4-aot-data-repositories">What are Spring Boot 4 AOT data repositories?</h2>
<p>Spring Boot 4 AOT data repositories are Spring Data repository implementations generated at build time, replacing the reflective runtime proxies that Spring Data has historically used.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/spring-boot-4-aot-data-repositories-pipeline.webp" alt="Spring Data AOT pipeline: query methods analyzed at build time, generated classes compiled with the application" width="1600" height="845"></p>
<p>When you write this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserRepository</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> JpaRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Long</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findByEmailContaining</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> fragment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findByEmailIgnoreCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    long</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> countByActiveTrue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The traditional Spring Data flow at startup:</p>
<ol>
<li>Scan your classpath for repository interfaces.</li>
<li>For each method, parse the name into a query.</li>
<li>Derive the JPQL or SQL.</li>
<li>Generate a CGLIB or JDK proxy that intercepts every call.</li>
<li>Inject the proxy as a Spring bean.</li>
</ol>
<p>All of that happens inside <code>applicationContext.refresh()</code>, every time the app starts.</p>
<p>With AOT repositories, steps 1 through 3 happen during the Maven or Gradle build. Step 4 generates a real class file named <code>UserRepositoryImpl__AotRepository</code> in the same package as your interface. Step 5 still happens at startup, but the bean it wires up is the pre-generated class, not a runtime proxy.</p>
<p>The startup that took 2.1 seconds because Hibernate, Spring Data, and validation all kicked in together is now closer to 1.4 seconds, in my testing. And the heap it occupied has shrunk because the proxy bookkeeping is gone.</p>
<h2 id="why-does-compile-time-generation-matter">Why does compile-time generation matter?</h2>
<p>Compile-time generation matters because it converts a category of work that scales with application size from "runtime cost on every startup" to "build cost paid once."</p>
<p>The motivation isn't just speed. It's also correctness and visibility.</p>
<p>The old runtime proxy model has three properties that hurt at scale. First, it's reflective, which means GraalVM native image needs explicit hints for every reflective call. Spring Boot has shipped those hints incrementally over years, but it's a moving target. Second, the generated SQL is invisible at code-review time. You can run the app and inspect the logs, but you can't grep for it. Third, when a method name doesn't parse cleanly, the failure shows up at startup, not at compile time. I've shipped a regression where a typo in a derived method name (<code>findByEmailIs</code> vs <code>findByEmailEqualTo</code>) only surfaced in the dev environment because it didn't break the build.</p>
<p>AOT repositories address all three. The reflection is gone (or at least dramatically reduced). The generated SQL is real Java code in <code>target/generated-sources/aot/</code> that you can read. And invalid method names break the build, not the application.</p>
<p>The Spring Data team's framing for it: "Generated query methods contain the exact same code you would write if you would not use Spring Data to run your query." That's not a stretch. The output is straightforward Java that calls into <code>EntityManager</code> (for JPA) or executes a <code>JdbcTemplate</code> query (for JDBC). No more wondering what the proxy did under the covers.</p>
<h2 id="how-do-you-enable-aot-repositories">How do you enable AOT repositories?</h2>
<p>AOT repositories are enabled by default when the Spring AOT engine is active. Two ways to activate the AOT engine:</p>
<p><strong>Maven build with AOT goal:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">plugin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">org.springframework.boot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">spring-boot-maven-plugin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">executions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">execution</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">process-aot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">goals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">goal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">process-aot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">goal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">goals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">execution</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">executions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">plugin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>After running <code>mvn package</code>, the generated AOT sources sit under <code>target/spring-aot/main/sources/</code>. Open one and you'll see the actual method bodies for your repository.</p>
<p><strong>Gradle build:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">plugins</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    id</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"org.springframework.boot"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    id</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"org.graalvm.buildtools.native"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// only if you want native image</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">springBoot</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    mainClass.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"com.example.Application"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Running <code>./gradlew bootJar</code> triggers the AOT pipeline.</p>
<p><strong>Native image:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">./mvnw</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -Pnative</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> native:compile</span></span></code></pre></figure>
<p>Or with Gradle:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">./gradlew</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> nativeCompile</span></span></code></pre></figure>
<p>In native mode, AOT repositories aren't optional. They're how the build avoids the reflective code paths that GraalVM can't analyze.</p>
<p>To turn AOT repositories off (for testing, or to compare behavior):</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="properties" data-theme="material-theme github-light"><code data-language="properties" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.aot.repositories.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">false</span></span></code></pre></figure>
<p>To disable for a specific module:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="properties" data-theme="material-theme github-light"><code data-language="properties" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.aot.jdbc.repositories.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.aot.jpa.repositories.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.aot.mongodb.repositories.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.aot.cassandra.repositories.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">false</span></span></code></pre></figure>
<p>In normal JVM mode without the AOT goal, your app keeps using the runtime proxies. Nothing forces you onto the AOT path until you opt in via the build plugin.</p>
<h2 id="what-gets-generated-at-build-time">What gets generated at build time?</h2>
<p>The build generates a Java class for each imperative repository interface, named <code>&#x3C;RepositoryName>Impl__AotRepository</code>, in the same package as the interface.</p>
<p>For the <code>UserRepository</code> example above, the generated class is <code>UserRepositoryImpl__AotRepository</code>. Each query method becomes a real Java method that calls into the persistence layer directly.</p>
<p>For Spring Data JPA, the generated method for <code>findByEmailContaining</code> looks roughly like this (simplified for clarity):</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findByEmailContaining</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fragment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jpql </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">SELECT u FROM User u WHERE u.email LIKE ?1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entityManager</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createQuery</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">jpql</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setParameter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">%</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fragment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">%</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getResultList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>For Spring Data JDBC, the equivalent generates a SQL string with parameterized binding. For MongoDB, it generates the document filter. For Cassandra, the CQL statement.</p>
<p>The generated code is technology-specific, which is the point. There's no abstraction layer at runtime, no proxy interception, no reflective <code>Method.invoke</code>. You get the same JPQL or SQL you'd write by hand.</p>
<p>What it does NOT generate: anything for reactive repositories. The build skips <code>ReactiveCrudRepository</code> and friends silently. Anything for <code>@Query</code>-annotated methods that use SpEL expressions evaluated at runtime. Anything for repository fragments backed by custom implementations (your <code>UserRepositoryCustom</code> impl class is left alone).</p>
<p>The metadata side of generation is also useful: Spring Data writes JSON files alongside the generated classes containing every query method's derived SQL. These are the same metadata Spring uses to wire native image runtime hints. If you want to audit every query your app issues without running it, those JSON files are the cleanest source.</p>
<p>A practical thing I do with the JSON metadata: feed it into a script that diffs the queries between two builds. Adding a method to a repository or renaming an existing one shows up as a clean diff in CI. This catches a category of accidental N+1 queries that I used to find only in production. The build now tells me when a derived query suddenly fans out into a per-row lookup instead of a single join. It's the kind of tooling I always wanted but never had a reasonable place to plug in. With AOT generation putting the SQL on disk, that integration point exists by default.</p>
<h2 id="how-do-they-work-with-graalvm-native-images">How do they work with GraalVM native images?</h2>
<p>Native images are the headline use case. AOT repositories make Spring Data viable in GraalVM native compilation by removing the reflection that native image can't follow.</p>
<p>GraalVM native image needs to know at build time what classes will be reflected on, what methods will be called dynamically, and what proxies will be created. Spring's traditional Data infrastructure relied heavily on all three. Even with reachability metadata files, edge cases would slip through and you'd hit <code>MissingReflectionRegistrationError</code> at runtime in the native binary.</p>
<p>With AOT repositories, the reflective machinery doesn't exist in the compiled binary. The repository is a regular class. The only reflection left is what Hibernate or the JDBC driver does for entity introspection, and Spring Boot ships proper hints for that.</p>
<p>I migrated a Spring Boot 3.x service to native image last year, and the longest single chunk of effort was tracking down the Spring Data reflection hints. Having tested the same migration on Spring Boot 4 with AOT repositories, the failure modes that took me days to debug are mostly gone. There are still a few corner cases (custom converters, audit listeners with reflection-based wiring), but the repository layer itself is no longer the rough part.</p>
<p>The startup numbers in native mode are dramatic. A native image that starts in 80ms vs a JVM application that starts in 1.5 seconds is the kind of win that changes how you think about scale-to-zero deployments. AOT repositories aren't the only reason that gap exists, but they're a significant chunk of it.</p>
<p>One specific optimization worth knowing: Spring Data uses <code>ManagedTypes</code> to enumerate the entity set at build time, because classpath scanning isn't available in native mode. As long as your entities are detected by the build (they should be if they're annotated with <code>@Entity</code> and on the classpath), the AOT pipeline collects them automatically. No manual entity registration.</p>
<h2 id="what-are-the-tradeoffs-and-limitations">What are the tradeoffs and limitations?</h2>
<p>The tradeoffs are real and worth knowing before you commit.</p>
<p><strong>Configuration is frozen at build time.</strong> The Spring Data team's exact words: the framework "trades certain dynamic aspects" for the speed. You can't generate database-specific SQL and swap databases without rebuilding. If you have an app that runs on Postgres in prod and H2 in tests with different schemas, the AOT-generated SQL is targeted to one of them.</p>
<p><strong>Reactive repositories aren't supported.</strong> If your stack is built on <code>ReactiveMongoRepository</code> or similar, AOT generation skips it entirely. The runtime proxy is still in use for those. Spring Data 2025.1 explicitly limits AOT to imperative interfaces. The team has signaled future work on reactive support but no timeline.</p>
<p><strong>Build time goes up.</strong> AOT processing isn't free. On a service with 40-odd repository interfaces, the AOT build phase added about 18 seconds to a previously 35-second build. Not catastrophic, but noticeable, and it scales with repository count. If you've got a monolith with hundreds of repositories, plan for a longer CI.</p>
<p><strong>Generated SQL can surprise you.</strong> The AOT pipeline derives SQL from method names, the same as the runtime proxy did. The difference is that with runtime proxies, you found out the SQL was wrong by reading logs. With AOT, you find out at build time when the metadata pipeline rejects an ambiguous method name. Most teams find this an upgrade. Some won't.</p>
<p><strong>Custom <code>@Query</code> SpEL expressions break.</strong> If you've leaned heavily on <code>@Query("...?#{principal.id}...")</code> style queries, those don't AOT-compile cleanly. The SpEL evaluation needs runtime context. The build will skip generation for those methods and fall back to the runtime path for them, which mostly works but means the performance benefit is partial.</p>
<p><strong>Debugging is different.</strong> The good news: you can read the generated source and step into it during a debug session. The bad news: stack traces now point to generated class names, which can confuse tooling that doesn't recognize them. IDEs handle this fine, but some external log aggregators may need configuration.</p>
<h2 id="when-should-you-use-aot-repositories">When should you use AOT repositories?</h2>
<p>Three cases where I'd turn them on without hesitation.</p>
<p><strong>If you're targeting GraalVM native image.</strong> AOT repositories are the path. Don't fight Spring Data's reflection on native; use the AOT pipeline that Spring Boot 4 ships for exactly this case.</p>
<p><strong>If startup time is on your scorecard.</strong> Lambda cold starts, Kubernetes pod scaling, anywhere you're paying for the time between "process started" and "ready to serve traffic." Imperative Spring Data + AOT cuts a big chunk.</p>
<p><strong>If you maintain a service with stable schemas.</strong> The "frozen at build time" tradeoff isn't a tradeoff if you're not switching databases or schemas at runtime, which describes 95% of services I've worked on.</p>
<p>Two cases where I'd hold off.</p>
<p><strong>If you're heavily reactive.</strong> No point. Wait for reactive AOT support.</p>
<p><strong>If your team relies on <code>@Query</code> SpEL expressions everywhere.</strong> The fallback is fine, but you won't see the full benefit, and the mixed mode (some methods AOT, some not) adds cognitive load.</p>
<p>The one decision I'd push every Spring Boot 4 project to make explicitly: turn on AOT in your build pipeline. Even if you're not deploying as native image, the build-time validation of repository method names is worth it. A typo that breaks startup in dev is fine. A typo that breaks startup in prod after a release is a problem you'd rather catch in CI.</p>
<p>The Spring Data team has shipped this feature quietly. It deserves more attention. If you're on Spring Boot 4 and haven't enabled it, do that this sprint.</p>
<hr>
<p>For the official details, see <a href="https://spring.io/blog/2025/11/25/spring-data-ahead-of-time-repositories-part-2/">Spring's blog post on AOT repositories part 2</a> and the <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/aot.html">Spring Data JPA AOT optimization docs</a>. The <a href="https://docs.spring.io/spring-data/commons/reference/aot.html">Spring Data Commons AOT reference</a> covers the cross-module pieces that apply regardless of which store you're using.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java with Spring Boot</a>. Background on the Java 21+ features Spring Boot 4 builds on, and how virtual threads pair with AOT for fast-start services.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide">Spring Boot Testcontainers Guide</a>. Real-database testing patterns that pair well with AOT repositories. AOT validates the SQL at build time, Testcontainers proves it works against the real engine.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Hibernate Lazy Init Guide</a>. What Hibernate is doing behind your repositories. The AOT pipeline doesn't change Hibernate's lazy loading semantics, but understanding them is still essential.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution">Spring Security Component Revolution</a>. The other major Spring Boot 4 architectural change worth knowing alongside AOT repositories.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Java 26 Structured Concurrency: What Changed in the Sixth Preview]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/java-26-structured-concurrency-jep-525</link>
      <guid>https://www.rabinarayanpatra.com/blogs/java-26-structured-concurrency-jep-525</guid>
      <pubDate>Tue, 12 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Java 26's JEP 525 refines structured concurrency with a new onTimeout joiner and List return types. Practical guide with code and virtual thread patterns.]]></description>
      <content:encoded><![CDATA[<p>I've been writing concurrent Java for a long time, and every team I've worked on has eventually run into the same thing: a thread leak that nobody can trace, an executor that's still running tasks ten minutes after the request that started them returned, a test that's flaky because of a race nobody can pin down.</p>
<p>Structured concurrency fixes the category of problems that traditional executors leave on the table. Java 26's JEP 525 brings it to the sixth preview, with two focused changes: a new onTimeout callback on Joiner, and a cleaner return type for the most common joiner. It's not finalized yet, but it's close, and the API is stable enough to use seriously in projects that can run with <code>--enable-preview</code>.</p>
<p>This post covers what structured concurrency actually is, what changed in this preview, and how I'm using it with virtual threads in real code.</p>
<h2 id="what-is-structured-concurrency-in-java-26">What is structured concurrency in Java 26?</h2>
<p>Structured concurrency in Java 26 is an API that confines the lifetime of a group of concurrent subtasks to a single lexical scope, ensuring every subtask is either complete or cancelled when the scope exits.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/java-26-structured-concurrency-scope.webp" alt="Structured concurrency scope: parent task forks subtasks that all complete or cancel together" width="1600" height="845"></p>
<p>The principal class is <code>StructuredTaskScope</code> in <code>java.util.concurrent</code>. You open a scope with <code>try-with-resources</code>, fork your subtasks inside it, join the scope as a unit, and the scope handles the rest. If one subtask fails, the scope can cancel the others. If you exit the scope without joining, you get an error rather than orphaned threads.</p>
<p>Here's the simplest case:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> StructuredTaskScope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Joiner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allSuccessfulOrThrow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userTask </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderTask </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetchOrders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> results </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserView</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Two subtasks, each running on its own virtual thread, both have to succeed for the scope to return. If <code>fetchUser</code> throws, the scope cancels <code>fetchOrders</code> and propagates the exception. If <code>fetchOrders</code> is still running when <code>fetchUser</code> succeeds, the scope waits for both. There's no thread that survives past the closing brace.</p>
<p>Compare that to the unstructured version with an <code>ExecutorService</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetchOrders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orders </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserView</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    userFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">cancel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    orderFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">cancel</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    throw</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> finally</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">shutdown</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That's seven lines of plumbing for a two-task fork-join. The structured version is three.</p>
<h2 id="what-changed-in-the-sixth-preview">What changed in the sixth preview?</h2>
<p>JEP 525 introduces two API refinements: an <code>onTimeout()</code> callback on the <code>Joiner</code> interface, and a cleaner return type for <code>Joiner.allSuccessfulOrThrow()</code>.</p>
<h3 id="why-does-the-new-ontimeout-joiner-method-matter">Why does the new onTimeout joiner method matter?</h3>
<p><code>onTimeout()</code> lets a custom Joiner react to a timeout and return a partial or fallback result rather than throwing. In earlier previews, hitting a timeout always meant an exception. That worked for hard deadlines, but it didn't fit the patterns that real systems use.</p>
<p>A common case: you have three downstream services to call. Two of them respond in 200ms. The third one is slow and you'd rather return a partial response than wait. With <code>onTimeout()</code>, you can build a Joiner that returns whatever subtasks succeeded by the deadline:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> FirstNCompleteJoiner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Joiner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> results </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CopyOnWriteArrayList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> FirstNCompleteJoiner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">target </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> onComplete</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">state</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">State</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SUCCESS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">add</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> >=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">copyOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> onTimeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">copyOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">results</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The Joiner accumulates successful results, finishes early once it has <code>target</code> of them, and returns whatever it has if the timeout fires before that. Without <code>onTimeout()</code>, the third path would have thrown.</p>
<p>This is the kind of pattern you can't express cleanly with a try/catch around the join call, because by the time you catch the exception, the partial results are gone.</p>
<h3 id="whats-the-new-return-type-for-allsuccessfulorthrow">What's the new return type for allSuccessfulOrThrow?</h3>
<p><code>Joiner.allSuccessfulOrThrow()</code> now returns a <code>List&#x3C;T></code> directly from <code>scope.join()</code> instead of a <code>Stream&#x3C;Subtask&#x3C;T>></code>. The fifth preview required you to map the stream to extract values:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Old (Java 25, fifth preview)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> subtasks </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> values </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> subtasks</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// New (Java 26, sixth preview)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> values </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>The old version was technically more flexible because <code>Subtask</code> carries state metadata. In practice, every codebase I've seen calls <code>.get()</code> on every subtask anyway, so the indirection was pure friction. The new return type matches what the common case actually needs.</p>
<h2 id="how-do-you-use-structuredtaskscope-in-production-code">How do you use StructuredTaskScope in production code?</h2>
<p>The factory method to use is <code>StructuredTaskScope.open(Joiner)</code>. You pick a Joiner based on what your task semantically needs. The built-in joiners cover the most common patterns.</p>
<p><code>Joiner.allSuccessfulOrThrow()</code>: every subtask must succeed. Returns a List of results in fork order. If any subtask fails, all others are cancelled and the exception propagates.</p>
<p><code>Joiner.anySuccessfulResultOrThrow()</code>: race the subtasks. The first successful result wins, the rest are cancelled. If all fail, you get the last failure. Useful for redundant fetches: query two replicas, take whichever responds first.</p>
<p><code>Joiner.awaitAll()</code>: wait for all subtasks to complete, regardless of outcome. You inspect each Subtask's state afterward to decide what to do. Useful when you want partial results even on failure.</p>
<p>Custom joiners (like the timeout-aware one above) handle the cases the built-ins don't cover.</p>
<p>A real example from a service I'm working on: fetching an order summary that needs a user record, the user's recent orders, and pricing data. All three from different services. If any fails, the whole request fails, but I don't want to wait for the slow ones in series.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> OrderSummary</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getOrderSummary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Duration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> timeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        throws InterruptedException </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> StructuredTaskScope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            Joiner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allSuccessfulOrThrow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            cf </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withTimeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">timeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userTask </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fetch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ordersTask </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fetch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Pricing</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pricingTask </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pricingService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fetch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">orderId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> OrderSummary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            userTask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            ordersTask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            pricingTask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The <code>cf.withTimeout(timeout)</code> configures the scope to cancel everything if the deadline is missed. The exception type is <code>StructuredTaskScope.TimeoutException</code>, distinct from any failures the subtasks themselves throw, so callers can react differently.</p>
<p>The throughput numbers from rewriting one of our internal aggregation endpoints with this pattern: median latency went from 720ms (sequential) to 280ms (parallel), with no extra thread pool to manage and no executor to shut down on application stop.</p>
<h2 id="how-does-it-work-with-virtual-threads">How does it work with virtual threads?</h2>
<p><code>StructuredTaskScope</code> uses virtual threads by default for every forked subtask. You don't configure it, you just fork.</p>
<p>That matters because virtual threads are designed for the case structured concurrency is built around: lots of independent I/O-bound subtasks. A platform thread pool that you'd previously size carefully (say, 200 threads for a high-traffic endpoint) becomes irrelevant. You can fork a thousand subtasks inside a scope and the JVM handles it.</p>
<p>The contract: each forked subtask runs on a fresh virtual thread. The thread is owned by the scope, not by an executor that outlives the scope. When the scope closes, every thread it created is gone.</p>
<p>Where this changes the way you write code: you stop reaching for <code>Executors.newFixedThreadPool</code> or <code>ForkJoinPool.commonPool()</code> for application-level fan-out. Those are still useful for genuinely shared work pools (think: a cron job that processes batches), but for request-scoped concurrency, the scope is the better fit.</p>
<p>A pattern I've started using: if a request handler needs to make N parallel calls, I open a scope, fork the calls, and let the scope manage the thread lifecycle. No more thread pool sizing decisions for request-scoped fan-out.</p>
<h2 id="how-do-you-handle-errors-and-partial-failures">How do you handle errors and partial failures?</h2>
<p>Error handling in structured concurrency hangs on which Joiner you choose, and JEP 525's <code>onTimeout</code> callback adds a third axis: timeout-as-success-with-partial-data.</p>
<p>For the all-or-nothing case, <code>Joiner.allSuccessfulOrThrow()</code> does what its name says. Any subtask failure throws on <code>join()</code>, all sibling subtasks are cancelled, and the original exception propagates with the others as suppressed exceptions. You see all of them in the stack trace, which is a huge debugging improvement over <code>CompletableFuture.allOf()</code> where only the first failure surfaces.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> StructuredTaskScope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Joiner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allSuccessfulOrThrow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IOException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">primary failed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> SQLException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">secondary failed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">StructuredTaskScope.FailedException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Throwable</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cause </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCause</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    Throwable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> suppressed </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cause</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getSuppressed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // cause is the first failure observed</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // suppressed has the others</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>For the partial-results case, <code>Joiner.awaitAll()</code> waits for every subtask regardless of outcome and returns nothing. You inspect each <code>Subtask</code> afterward to decide what to do.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> StructuredTaskScope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Joiner</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">awaitAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> primary </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> primarySource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fetch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fallback </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fallbackSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fetch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">primary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">state</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">State</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SUCCESS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> primary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">fallback</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">state</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Subtask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">State</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SUCCESS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Primary source failed, using fallback</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> primary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fallback</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ServiceUnavailableException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">All sources failed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This pattern shows up in any system that has a primary and a fallback path. Without structured concurrency, you'd typically run them sequentially (slow when the primary is slow) or kick off both manually and manage cancellation by hand.</p>
<p>The third axis is the new <code>onTimeout</code> callback. With a custom Joiner that returns partial results on timeout, you get a clean way to express "fetch as much as you can in N milliseconds, then return what you have." That's a pattern I've previously had to build with <code>CompletableFuture.applyToEither</code> and explicit timer scheduling, and it was always brittle. The Joiner-based version is something you can read and trust.</p>
<p>One thing to watch out for: the cancellation that the scope triggers on failure isn't instant. It interrupts the threads, but a subtask doing CPU-bound work that ignores interrupts will run to completion. Structured concurrency doesn't change Java's cooperative cancellation model. If you're forking work that doesn't respond to interrupts, the scope won't be able to clean it up promptly.</p>
<p>The fix is the same one you'd apply anywhere: write code that checks <code>Thread.interrupted()</code> periodically, or call into APIs that throw <code>InterruptedException</code>. The scope can ask threads to stop, but it can't force them.</p>
<h2 id="when-should-you-use-structured-concurrency-vs-alternatives">When should you use structured concurrency vs alternatives?</h2>
<p>The decision tree I use:</p>
<p><strong>Use structured concurrency when</strong> the subtasks share a lifetime with the parent task. Request handlers, batch processors that fan out per-item work, anything where "all subtasks should be done when this method returns" is a hard requirement.</p>
<p><strong>Use a long-lived ExecutorService when</strong> the work outlives the request. Background jobs, queue processors, scheduled tasks that run on a cron. The scope model doesn't fit because there's no parent task to close it.</p>
<p><strong>Use CompletableFuture chaining when</strong> you need fine-grained composition: this task's output flows into that task, with branching and merging. The scope model is for fan-out and join, not pipelines.</p>
<p>The case where I'd specifically pick structured concurrency over <code>CompletableFuture.allOf()</code>: when error propagation matters. With <code>allOf</code>, a failure in one future doesn't cancel the others. With <code>StructuredTaskScope</code>, it does, by default. That's usually what you want for request-scoped work, and getting it right with <code>CompletableFuture</code> requires manual cancellation logic.</p>
<p>The case where I'd specifically use it over <code>parallelStream()</code>: when the tasks are I/O-bound. <code>parallelStream</code> runs on the common ForkJoinPool, which is sized for CPU work and pinned to platform threads. Forking I/O work onto it backpressures the rest of the JVM. Structured concurrency on virtual threads doesn't have that problem.</p>
<h2 id="whats-coming-in-the-seventh-preview">What's coming in the seventh preview?</h2>
<p>JEP 533 is already in flight as the seventh preview, targeting a future Java release. It refines the API further based on feedback from the sixth preview, but the core shape is stable.</p>
<p>The signal that matters: structured concurrency has been in preview since Java 19. Six previews is a lot, but the API has changed substantially through that period. JEP 525's changes are small and targeted, which suggests the design is settling. I'd expect a final release in the next LTS window.</p>
<p>For Java 26 specifically, this is a feature I'd turn on with <code>--enable-preview</code> for new internal services. The API isn't going to break in ways that hurt, and you get the correctness benefits today. For libraries you publish, I'd still wait for the final release.</p>
<p>The longer-term picture: structured concurrency makes virtual threads usable in a way that traditional thread pools didn't. Virtual threads alone solve the "how do I have a million threads" problem. Structured concurrency solves the "how do I keep them sane" problem. Together they're the first concurrency model in Java that I'd actually recommend without caveats.</p>
<hr>
<p>For the official spec, see <a href="https://openjdk.org/jeps/525">JEP 525: Structured Concurrency (Sixth Preview)</a> and <a href="https://www.infoq.com/news/2026/01/timeout-joiner-refinements/">InfoQ's coverage of the timeout joiner refinements</a>. For background on how the API evolved, <a href="https://openjdk.org/jeps/505">JEP 505 covered the fifth preview</a> and <a href="https://bazlur.ca/2026/01/04/structured-concurrency-in-java-26-api-polishing-timeouts-and-better-joiners/">Bazlur Rahman's deep dive on the timeout changes</a> walks through the design decisions in detail.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Virtual Threads in Java 25: A Practical Guide</a>. The thread model that structured concurrency builds on. Read this first if virtual threads are still new to you.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-26-http-3-httpclient">Java 26 HTTP/3 in HttpClient</a>. The other major Java 26 release feature, useful when you're forking subtasks that hit external services.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-25-compact-object-headers">Java 25 Compact Object Headers</a>. Lower-level Java 25 work that pairs with structured concurrency for memory-conscious services.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Claude Code Routines: Async CI Automation Just Became Real]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/claude-code-routines-ci-automation</link>
      <guid>https://www.rabinarayanpatra.com/blogs/claude-code-routines-ci-automation</guid>
      <pubDate>Fri, 08 May 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Claude Code Routines automate PR reviews, CI fixes, and nightly triage on Anthropic's cloud. Here's everything that shipped at Code with Claude 2026.]]></description>
      <content:encoded><![CDATA[<p>I opened my laptop Wednesday morning to a PR that had already fixed its own CI failure. Claude had seen the red X, read the error, written the fix, and pushed it to the branch. I hadn't touched anything since the night before.</p>
<p>That's the idea behind Routines, the standout announcement from Anthropic's Code with Claude 2026 conference on May 6 in San Francisco. And if you're using Claude Code regularly, it changes how your days actually run.</p>
<p>This post covers what shipped, how the individual features work, and what you should actually try first.</p>
<h2 id="what-are-claude-code-routines">What are Claude Code Routines?</h2>
<p>Routines are saved Claude Code configurations that trigger on a schedule, an API call, or a GitHub event, running on Anthropic's cloud infrastructure while your laptop is off.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-code-routines-workflow.webp" alt="Claude Code Routines workflow: trigger sources feeding into cloud-run automations" width="1600" height="845"></p>
<p>The typical use cases: a nightly scan of open PRs flagging anything stale, an automatic code review triggered when a PR opens against main, a weekly dependency audit that files issues for anything out of date, or a doc sync that runs after every merge to keep documentation aligned with the code.</p>
<p>Boris Cherny, who leads Claude Code at Anthropic, described the target experience: "Developers can setup async automations and wake up to PRs that are ready to merge." You finish your day, the work continues without you, and you pick it up in the morning with less noise.</p>
<p>The key difference from a simple cron job is that Claude has full context of your repository. It isn't running a script with hard-coded rules. It's reading your code, your PR descriptions, your test output, and making judgment calls the same way it would in an interactive session.</p>
<h3 id="what-kinds-of-automations-make-sense">What kinds of automations make sense?</h3>
<p>The clearest wins are for work that's mechanical and repetitive but still requires reading code:</p>
<ul>
<li><strong>Morning PR triage</strong>: Claude reviews every open PR, summarizes what's blocking it, and flags the ones that need your attention.</li>
<li><strong>Nightly CI failure analysis</strong>: If builds broke overnight, Claude reads the logs, categorizes the failures, and prepares a report.</li>
<li><strong>Post-merge doc sync</strong>: After a PR merges, Claude checks whether the docs still match the changed code and opens an issue (or a PR) if they don't.</li>
<li><strong>Security review on open PRs</strong>: Claude runs a security pass on any PR that touches auth, payments, or data handling.</li>
</ul>
<p>These are things most teams want but never get around to automating because the tooling complexity isn't worth it for a one-person project. Routines make it worth it.</p>
<h2 id="how-does-the-ci-auto-fix-feature-work">How does the CI auto-fix feature work?</h2>
<p>When CI fails on your PR, Claude reads the error output, investigates what broke, writes a fix, and pushes it to the branch with an explanation of what it changed. The stated goal from the team: "The person who owns the PR is never going to see a red X."</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-code-routines-ci-fix.webp" alt="CI auto-fix pipeline: failure detection, diagnosis, fix commit, and push" width="1600" height="845"></p>
<p>In practice, Claude is watching your PR pipeline. A test failure gets diagnosed and patched. A lint error gets fixed and pushed. An import that broke after a rename gets traced back and updated. It's not redesigning your code. It's handling the mechanical cleanup that usually eats 15 minutes of your morning.</p>
<p>A companion feature, auto-merge, takes it one step further. Once all checks pass, Claude can merge the PR automatically. You configure whether you want the auto-fix only, the auto-merge only, or the full pipeline. The default is conservative: fix and wait for your approval to merge.</p>
<p>A few things to understand about what it won't do well: if CI is failing because a database migration conflicts with a new schema expectation, that's a judgment call Claude shouldn't make alone. If the failure is architectural, it'll flag it and wait rather than guess. The feature is designed for the common case of fixable failures, and from my experience, that covers the majority of CI red marks on any active branch.</p>
<h2 id="how-does-code-review-work-in-the-desktop-app">How does Code Review work in the desktop app?</h2>
<p>Claude Code Review shipped alongside Routines and is already in use across every team at Anthropic. It reviews your local diff before you push, leaving comments in the desktop diff view directly.</p>
<p>The comments surface bugs it spotted, suggestions for the change, and potential issues. You can ask Claude to address any of its own comments and it'll revise the diff. It's an interactive loop before the code ever leaves your machine.</p>
<p>I've been using it on this portfolio's codebase and the most useful thing it does is catch what you stop seeing after staring at the same file for an hour. A variable that shadows a parameter. A missing null check on an API response. The kind of thing that gets through your own review because your brain fills in the expected behavior.</p>
<p>It doesn't replace a human reviewer for architectural decisions. For the mechanical layer of review, though, it's catching things before they hit your team and before they hit CI.</p>
<h2 id="what-else-shipped-at-code-with-claude-2026">What else shipped at Code with Claude 2026?</h2>
<p>Beyond the workflow automation features, a few other changes are worth knowing.</p>
<p><strong>Rate limits</strong>: Claude Code five-hour limits were doubled for Pro, Max, Team, and Enterprise plans. Peak-hours throttling for Pro and Max accounts is gone. Opus API rate limits got a substantial increase.</p>
<p><strong>Capacity</strong>: The rate limit headroom comes from the SpaceX Colossus data center partnership. Anthropic is taking on more than 300 megawatts from Colossus, translating to over 220,000 NVIDIA GPUs coming online within the month. The API volume signal that drove this: Anthropic's API usage is up 17x year over year.</p>
<p><strong>Managed agent capabilities</strong>: Anthropic announced three new features for multi-agent work. Multi-agent orchestration (public beta) lets you spin up fleets of agents for complex tasks. Outcomes (public beta) lets you set success criteria so Claude iterates independently until it meets them. Dreaming (research preview) is more experimental: Claude inspects its own previous sessions, identifies what it missed, and self-improves over time.</p>
<p><strong>The scale context</strong>: Mercado Libre has 23,000 engineers and is targeting 90% autonomous coding by Q3 2026. That number tells you what direction the industry is moving, and why Anthropic needed the Colossus capacity this month.</p>
<p>The conference itself continues: London on May 19, Tokyo on June 10. Additional features may ship through May.</p>
<h2 id="where-should-you-start-with-these-features">Where should you start with these features?</h2>
<p>If you're already on Claude Code Max or Pro, the doubled rate limits are already in effect. Nothing to configure.</p>
<p>For Routines and CI auto-fix, rollout is happening through May. The fastest way to evaluate CI auto-fix: pick a branch where you're actively working and CI fails regularly. Let Claude handle the mechanical failures. You'll see quickly what it gets right and where it needs a clearer repo setup.</p>
<p>For Code Review, it's in the desktop app now. Open a diff you're about to push, run the review, and compare what it flags to your own manual pass. You'll know in about five minutes whether it's catching things you'd miss.</p>
<p>The one thing I'd push back on: it's tempting to automate everything immediately. Start with one Routine that solves a specific pain point, watch how it behaves over a week, then add more. Claude Code has full repo context, but the automation is only as good as the task definition you give it.</p>
<p>My setup: CI auto-fix on the dev branch, morning PR summary routine, and Code Review before every push. That's it for now. The rate limits being doubled means I'm not holding back on interactive sessions to save headroom, which is the real quality-of-life change from this release.</p>
<hr>
<p>For more details on how Routines and CI auto-fix work, see <a href="https://code.claude.com/docs/en/overview">Claude Code's documentation at code.claude.com</a> and <a href="https://simonwillison.net/2026/May/6/code-w-claude-2026/">Simon Willison's Code with Claude 2026 live blog</a>. Anthropic's blog post <a href="https://claude.com/blog/preview-review-and-merge-with-claude-code">Preview, review, and merge with Claude Code</a> covers the desktop Code Review and auto-merge pipeline.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects: What's the Difference?</a>. Understand how Routines fit into Claude's broader tool architecture before building automations on top of it.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway Deep Dive</a>. If you're building on the Claude API behind Routines, the AI Gateway handles routing, fallbacks, and observability at the API layer.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Claude Opus 4.7 Release and Migration Guide</a>. The model powering these features, and what changed in the version your Routines will run on.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Next.js 16.2 AGENTS.md and next-browser: A Hands-On Guide]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/nextjs-16-2-agents-md-next-browser</link>
      <guid>https://www.rabinarayanpatra.com/blogs/nextjs-16-2-agents-md-next-browser</guid>
      <pubDate>Tue, 28 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Next.js 16.2 ships AGENTS.md by default, bundles the full docs in node_modules/next/dist/docs, and adds next-browser CLI for AI agents. Hands-on guide.]]></description>
      <content:encoded><![CDATA[<p>I run Claude Code on my Next.js portfolio every day. For the last year my routine has been the same: paste the relevant Next.js 16 doc into chat, watch the agent write something that worked in Next.js 13, correct it, paste another doc, repeat. The agent's training data was stale, the context window was precious, and I was the middleman.</p>
<p>Next.js 16.2 quietly killed that routine. Shipped on March 18, 2026 by Jude Gao, Jimmy Lai, Tim Neutkens and the rest of the Next.js team, the release ships <code>AGENTS.md</code> by default, bundles the entire Next.js docs as plain Markdown inside <code>node_modules/next/dist/docs/</code>, and adds an experimental <code>@vercel/next-browser</code> CLI that lets agents inspect a running app through shell commands. Vercel ran their internal evals and saw 100% pass rate with <code>AGENTS.md</code> vs 79% with the best skill-based setup. That is a big enough gap to change how I scaffold projects.</p>
<p>This post walks through what actually shipped, how to add it to an existing project, and what I changed in my own workflow after upgrading <code>rabinarayanpatra.com</code> to 16.2.</p>
<h2 id="what-is-agentsmd-in-nextjs-162">What is AGENTS.md in Next.js 16.2?</h2>
<p><code>AGENTS.md</code> is a Markdown file at the root of a Next.js project that tells AI coding agents to read the version-matched docs bundled inside <code>node_modules/next/dist/docs/</code> before writing any code. It is not a skill, it is not a plugin, and it is not loaded on demand. It is always-on context that any agent respecting the <code>AGENTS.md</code> convention will pick up at the start of every turn.</p>
<p>The default file that <code>create-next-app</code> generates is small. Here is the shape of it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="md" data-theme="material-theme github-light"><code data-language="md" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">&#x3C;!-- BEGIN:nextjs-agent-rules --></span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold"># </span><span style="--shiki-dark:#FFCB6B;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold">Next.js: ALWAYS read docs before coding</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Before any Next.js work, find and read the relevant doc in </span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">node_modules/next/dist/docs/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">`</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">. Your training data is outdated. The docs are the source of truth.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">&#x3C;!-- END:nextjs-agent-rules --></span></span></code></pre></figure>
<p>The <code>BEGIN</code> and <code>END</code> comment markers delimit the Next.js-managed section. Future codemods will only rewrite what is inside those markers, so anything you add outside stays yours. That is a small design choice with a big payoff, because it means you can keep your own agent rules next to the framework's without fighting merge conflicts.</p>
<p>The second piece is the bundled docs themselves. The <code>next</code> npm package now ships the full doc tree as plain Markdown files. Open <code>node_modules/next/dist/docs/</code> in your editor and you will see the same content as nextjs.org, organized by section. A compressed index file is also shipped at around 8 KB, an 80% reduction from the raw 40 KB of full docs, which is what the agent reads first to find the right file to pull into context.</p>
<p>Third, on projects already set up for Claude Code, there is a tiny convention link:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="md" data-theme="material-theme github-light"><code data-language="md" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">@AGENTS.md</span></span></code></pre></figure>
<p>The <code>@</code> directive tells Claude Code to include the contents of <code>AGENTS.md</code> whenever it loads <code>CLAUDE.md</code>. So the bootstrap chain becomes <code>CLAUDE.md</code> pulls in <code>AGENTS.md</code>, <code>AGENTS.md</code> tells the agent to read <code>node_modules/next/dist/docs/</code> whenever Next.js work is happening, and the agent writes code against the real 16.2 API rather than a memory of Next.js 13.</p>
<h2 id="how-do-you-add-agentsmd-to-an-existing-nextjs-project">How do you add AGENTS.md to an existing Next.js project?</h2>
<p>On Next.js 16.2 or later, you already have the docs on disk. You just need to create the two files yourself or let the codemod do it. The codemod is the faster path:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @next/codemod@latest</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> agents-md</span></span></code></pre></figure>
<p>That scaffolds <code>AGENTS.md</code> with the managed directive block and creates or updates <code>CLAUDE.md</code> with the <code>@AGENTS.md</code> include. I ran it on the portfolio and it was a one-second change plus two new files in the diff.</p>
<p>If you are on an older version of Next.js, upgrade first:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> @next/codemod@canary</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> upgrade</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> latest</span></span></code></pre></figure>
<p>Then re-run the <code>agents-md</code> codemod. The upgrade is where the doc bundle actually lands on disk, so you need 16.2 for the whole flow to work.</p>
<p>The part I liked most is the escape hatch. If your project already has a sprawling <code>AGENTS.md</code> with your own conventions, you can drop the managed block into it and keep everything else. The codemod respects existing files and only touches what is inside the markers. That matters for teams who have been writing their own agent rules for months.</p>
<p>One pitfall I hit on my first attempt: my <code>.gitignore</code> had <code>node_modules/</code> ignored (obviously), but Claude Code was still happy to read from it, since it is a local file read, not a git operation. So you do not need to commit the docs, you just need the package installed. CI builds, however, need to run <code>npm install</code> before an agent can use the docs, which is the usual order anyway.</p>
<h2 id="what-does-the-vercelnext-browser-cli-actually-do">What does the @vercel/next-browser CLI actually do?</h2>
<p><code>@vercel/next-browser</code> is an experimental CLI that wraps a persistent Chromium instance with React DevTools pre-loaded and exposes browser data through shell commands. Instead of an agent trying to understand a DevTools panel it cannot see, the agent runs a shell command, gets back structured text, and reasons about the output.</p>
<p>You install it as a skill:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> skills</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> add</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> vercel-labs/next-browser</span></span></code></pre></figure>
<p>Then trigger it inside an agent that supports skills by typing <code>/next-browser</code>. Claude Code and Cursor both work. The first run boots a Chromium instance you do not see, loads React DevTools into it, and holds the session open across commands.</p>
<p>At release the feature set covers five buckets. Component trees with props, hooks, state, and source-mapped file locations. PPR shell analysis, which identifies what is static and what is blocking. Errors and logs from the dev server. Network activity since the last navigation, including server actions. And visual capture with screenshots or loading filmstrips.</p>
<p>The concrete example in the release post is my favorite, because it shows the workflow end-to-end. You have a blog post page with a visitor counter:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> generateStaticParams</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> posts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getAllPosts</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> posts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> BlogPost</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getPost</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">slug</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> views</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getVisitorCount</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">slug</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // per-request</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">article</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">h1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">h1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">views</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> views</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">article</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Every slug is enumerated ahead of time, so the post content should prerender at build. But <code>getVisitorCount</code> runs on every request and sits at the top level, which drags the entire page out of the static shell. The user sees a loading skeleton instead of the post content streaming in.</p>
<p>An agent can diagnose this by locking PPR mode and inspecting the shell:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">next-browser</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ppr</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> lock</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">next-browser</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> goto</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /blog/hello</span></span></code></pre></figure>
<p>With PPR locked, only the static shell renders. In this case the shell is the loading skeleton, because nothing from the page made it in. Running <code>ppr unlock</code> gives the agent a structured report:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># PPR Shell Analysis</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># 1 dynamic hole, 1 static</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">blocked</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> by:</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">  -</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> getVisitorCount</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (server-fetch)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    owner:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> BlogPost</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> at</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> app/blog/[slug]/page.tsx:5</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    next</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> step:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Push</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> the</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> fetch</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> into</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> a</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> smaller</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Suspense</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> leaf</span></span></code></pre></figure>
<p>The agent now knows what the blocker is, where it lives, and what to do. It wraps the counter in a Suspense boundary:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> BlogPost</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getPost</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">slug</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">article</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">h1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">h1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> fallback</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">... views</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">span</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">VisitorCount</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">slug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;/</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">div</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">article</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Run <code>ppr lock</code> again and the shell has grown. The post content prerenders instantly, and only the view count falls back to the Suspense placeholder. The agent did that without ever opening a browser window.</p>
<p>The thing I keep coming back to is that the CLI is designed around one-shot commands against a persistent session. That matches how LLM tool-calling works today. The agent does not manage browser state, the CLI does. The agent just asks questions and parses replies, which is exactly the interaction pattern that works for a model.</p>
<h2 id="how-does-the-dev-server-lock-file-help-ai-agents">How does the dev server lock file help AI agents?</h2>
<p>Next.js 16.2 writes the running dev server's PID, port, and URL into <code>.next/dev/lock</code>. When a second <code>next dev</code> starts in the same directory, Next.js reads the lock file and prints an actionable error instead of a generic port-conflict message:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Error:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Another</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> next</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> dev</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> server</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> is</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> already</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> running.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">-</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Local:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">        http://localhost:3000</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">-</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> PID:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">          12345</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">-</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Dir:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">          /path/to/project</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">-</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Log:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">          .next/dev/logs/next-development.log</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Run</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> kill</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 12345</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> to</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> stop</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> it.</span></span></code></pre></figure>
<p>On paper this is quality-of-life for humans. In practice, this is the single change that saves me the most time with Claude Code.</p>
<p>Before 16.2, my workflow was: I start the dev server in a pane, Claude Code spawns its own <code>next dev</code> to check something, the command hangs because port 3000 is taken, Claude Code retries on another port, I get a second server I do not want. Now the agent gets a clean error, a PID to kill, a URL to connect to, and a log path to tail. It makes the right call the first time.</p>
<p>The lock file also prevents two <code>next build</code> processes from running at once, which could otherwise corrupt build artifacts. That is a genuine bug I have seen in CI where a re-run was queued before the first finished. Now the second run refuses to start.</p>
<h2 id="how-do-you-forward-browser-logs-to-the-terminal">How do you forward browser logs to the terminal?</h2>
<p>By default, Next.js 16.2 forwards browser errors to the dev terminal. You do not have to switch to the browser console to see client-side errors, which is another quality-of-life win for agents who cannot open a DevTools panel at all.</p>
<p>The level is configurable:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ts" data-theme="material-theme github-light"><code data-language="ts" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> nextConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  logging</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    browserToTerminal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // 'error': errors only (default)</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // 'warn':  warnings and errors</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // true:    all console output</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // false:   disabled</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> default</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> nextConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>I ran this at <code>true</code> for a week and it was too noisy for a typical debugging session. I rolled it back to <code>'warn'</code> which is the right default for me. Errors alone is usually too narrow, because half the client-side issues I chase start as warnings that the agent would have caught earlier with a wider filter.</p>
<p>Pairing this with the agent DevTools is the real win. The agent gets errors in the terminal, runs <code>next-browser</code> for a closer look, fixes the code, and moves on. No browser console, no screenshots to interpret, no back-and-forth with me asking what the page looked like.</p>
<h2 id="why-does-vercel-say-agentsmd-beat-skills-in-their-evals">Why does Vercel say AGENTS.md beat skills in their evals?</h2>
<p>The numbers from Vercel's internal eval are worth quoting straight. They tested four configurations against a suite of Next.js 16 APIs that did not exist in model training data, like <code>'use cache'</code>, <code>cacheTag()</code>, <code>forbidden()</code>, and <code>proxy.ts</code>:</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/nextjs-16-2-agents-md-next-browser-eval.webp" alt="Pass rate on Next.js 16 evals across four configurations: baseline 53 percent, skill default 53 percent, skill with explicit instructions 79 percent, AGENTS.md 100 percent" width="1600" height="845"></p>
<table>
<thead>
<tr>
<th>Configuration</th>
<th>Pass rate</th>
</tr>
</thead>
<tbody>
<tr>
<td>Baseline (no docs)</td>
<td>53%</td>
</tr>
<tr>
<td>Skill (default invocation)</td>
<td>53%</td>
</tr>
<tr>
<td>Skill with explicit instructions</td>
<td>79%</td>
</tr>
<tr>
<td>AGENTS.md docs index</td>
<td>100%</td>
</tr>
</tbody>
</table>
<p>The skill with default invocation matched baseline exactly. Vercel's explanation for that is brutal: "In 56% of eval cases, the skill was never invoked." The agent did not know it needed to look something up, so it did not.</p>
<p>Explicit instructions pushed skills to 79%, which is a real improvement, but it took careful prompt wording to get there. Phrasing like "You MUST invoke" caused agents to anchor on the docs and miss project context. "Explore project first, then invoke skill" was better. That kind of prompt sensitivity is not something I want to maintain for a team.</p>
<p><code>AGENTS.md</code> hit 100% across build, lint, and test eval categories. The authors called out the mental model: the goal is to shift agents "from pre-training-led reasoning to retrieval-led reasoning." Always-on context wins over on-demand retrieval because there is no decision point the agent can get wrong.</p>
<p>That matches my experience. The moments I lost the most time with Claude Code were the moments it was confidently wrong, using old APIs it remembered from training. It did not ping the docs because it did not know it needed to. <code>AGENTS.md</code> removes that failure mode by making the docs non-negotiable.</p>
<h2 id="what-changes-did-nextjs-162-make-to-rendering-performance">What changes did Next.js 16.2 make to rendering performance?</h2>
<p>The agent story is the headline, but 16.2 also ships a big performance jump that is easy to miss. The team landed a change in React (PR #35776) that replaces <code>JSON.parse</code> with a reviver callback with <code>JSON.parse</code> followed by a recursive walk in pure JavaScript. The result is up to 350% faster RSC payload deserialization and 25% to 60% faster real-world server rendering.</p>
<p>The root cause is a V8 quirk. <code>JSON.parse</code> with a reviver crosses the C++/JavaScript boundary once per key-value pair, and even a no-op reviver makes parsing around 4x slower than without one. Replacing that with a two-step parse-then-walk eliminates the per-key cost.</p>
<p>The measured numbers are the kind of data I trust because they come from real apps:</p>
<table>
<thead>
<tr>
<th>Workload</th>
<th>Before</th>
<th>After</th>
<th>Speedup</th>
</tr>
</thead>
<tbody>
<tr>
<td>1000-item Server Component table</td>
<td>19ms</td>
<td>15ms</td>
<td>26%</td>
</tr>
<tr>
<td>Server Component with nested Suspense</td>
<td>80ms</td>
<td>60ms</td>
<td>33%</td>
</tr>
<tr>
<td>Payload CMS homepage</td>
<td>43ms</td>
<td>32ms</td>
<td>34%</td>
</tr>
<tr>
<td>Payload CMS with rich text</td>
<td>52ms</td>
<td>33ms</td>
<td>60%</td>
</tr>
</tbody>
</table>
<p>You do not need to change any code. Upgrade to 16.2 and <code>react@latest</code> and the speedup lands. The heavier your RSC payload, the bigger the win, which is why the rich-text case hits 60%.</p>
<p>Next.js 16.2 also makes <code>ImageResponse</code> 2x faster for basic images and up to 20x faster for complex ones, with the default font switched from Noto Sans to Geist Sans. Dev startup is around 87% faster than 16.1 on the default application, so the time between <code>next dev</code> and a ready localhost shrank noticeably on my machine.</p>
<h2 id="does-agentsmd-replace-claude-code-skills-entirely">Does AGENTS.md replace Claude Code skills entirely?</h2>
<p>No, and I do not think it should. <code>AGENTS.md</code> and skills solve different problems.</p>
<p><code>AGENTS.md</code> is for always-on framework context. Your agent needs to know Next.js 16 every time it writes a Next.js component. That is not an on-demand concern, it is a default concern. Putting it in <code>AGENTS.md</code> removes the decision point about whether to fetch the docs, which is exactly the decision agents are bad at.</p>
<p>Skills are for scoped, opt-in capabilities. A skill that runs a specific CLI, calls a private API, or triggers a deploy is something you want the agent to invoke deliberately. The decision point is the feature. You do not want the agent triggering your prod deploy on every turn.</p>
<p>The Vercel team made the right call putting framework docs into <code>AGENTS.md</code> and keeping the <code>next-browser</code> debugger as a skill you trigger with <code>/next-browser</code>. The debugger is a scoped capability. The framework docs are ambient context. Matching each tool to the right mechanism is the actual lesson here.</p>
<p>My working rule, after a week of running both on the portfolio: if your agent should consult it every time, put it in <code>AGENTS.md</code>. If your agent should consult it sometimes, make it a skill.</p>
<h2 id="what-does-this-mean-for-the-next-year-of-framework-design">What does this mean for the next year of framework design?</h2>
<p>Next.js 16.2 is the first mainstream framework release where the primary design target is an AI coding agent, not a human developer. The default scaffolding exports a file the human may never read. The docs are shipped as Markdown because that is what parses cleanly for a model. The dev server writes a lock file so another process can recover without a human. The experimental DevTools are a shell command interface because that is what LLMs can drive.</p>
<p>This is not a future-looking take. It is already in <code>create-next-app@latest</code>. And the concrete UX win for me was small but sharp: I stopped pasting docs into chat. That one behavior change saved me an hour a week. Scaled across a team of developers all running agents all day, the time savings compound fast.</p>
<p>The open question is how many frameworks follow. Spring, FastAPI, Rails, Django, Laravel, every major framework has the same training-data-staleness problem Next.js just solved. I expect the next wave of releases to ship their own <code>AGENTS.md</code> conventions, bundled docs, and agent-friendly CLI wrappers. Vercel's eval numbers make the business case too strong to ignore.</p>
<p>For more on this release, see the <a href="https://nextjs.org/blog/next-16-2">Next.js 16.2 blog post</a>, the <a href="https://nextjs.org/blog/next-16-2-ai">Next.js 16.2 AI Improvements post</a>, and Vercel's <a href="https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals">AGENTS.md outperforms skills eval writeup</a>. The RSC payload perf change is <a href="https://github.com/facebook/react/pull/35776">React PR #35776</a> if you want to read the actual diff.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a Modern Docs Generator with Next.js 16</a>. Where I first went deep on Next.js 16's App Router and how I think about docs infrastructure.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">Hello Proxy in TypeScript and Next.js 16</a>. The middleware/proxy rename that landed in 16.0, relevant background for anything using <code>proxy.ts</code>.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/replacing-useeffect-data-fetching-server-actions">Replacing useEffect Data Fetching with Server Actions</a>. Server-side patterns that pair naturally with the 16.2 rendering improvements.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway Deep Dive</a>. The other side of the Vercel AI story, if you want the provider routing and observability angle.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Postgres Connection Pool Sizing: How PgBouncer Saved My Launch]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/postgres-connection-pool-pgbouncer-survival-guide</guid>
      <pubDate>Fri, 24 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[A real war story on Postgres connection pool sizing: why pool=500 nearly killed my launch, and how PgBouncer transaction mode multiplexed 5000 clients onto 25 backends.]]></description>
      <content:encoded><![CDATA[<p>In my previous org, we were a few days from a launch deadline when staging started falling over under load tests. I spent the first few hours convinced it was bad SQL. I combed through slow query logs, rolled back two recent migrations, and came up empty. The QA team kept pinging me, the PM kept asking for an ETA, and I had no answer.</p>
<p>Turned out it was one line in a YAML file. Connection pool size set to 500 on a 4-core Postgres box.</p>
<p>That is the kind of config that looks sensible until you do the math. Hibernate's Hikari pool default is 10. We had bumped it to 500 because someone had read a Stack Overflow answer about "scaling Postgres" and wanted headroom. Multiply that across ten app pods and you are not running a database anymore, you are running a fork bomb. Postgres forks a process per connection, each one eating around 10 MB of RAM, and they all fight for the same four CPU cores. Throughput collapsed long before we hit any real query bottleneck.</p>
<p>The fix that saved that launch was PgBouncer in transaction pooling mode. Five thousand app connections multiplexed onto 25 real Postgres connections. Same throughput. A fraction of the load. We shipped on time, and I spent the next month writing internal docs so the next person would not learn this the hard way.</p>
<p>This post is the long version of those docs. Pool size is the one config that quietly decides if your service survives production traffic, and it is one of the most consistently misconfigured knobs I see in code reviews. I will walk through why Postgres has hard limits, how to size your pool from first principles, what PgBouncer actually does, the sharp edges in transaction mode that will bite you, a config snippet you can copy, and when to reach for something beyond PgBouncer.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/postgres-connection-pool-pgbouncer-survival-guide.webp" alt="A diagram showing how thousands of application connections multiplex through PgBouncer into a small number of real Postgres backends" width="1600" height="845"></p>
<h2 id="why-does-postgres-choke-on-too-many-connections">Why does Postgres choke on too many connections?</h2>
<p>Postgres choke on too many connections because every connection is a full operating-system process, and processes are not free. The supervisor (postmaster) calls <code>fork()</code> for each new client, and the child becomes a backend that accumulates its own private state: relation cache, plan cache, prepared statement cache, work_mem allocations, temp buffers, and page table entries. Linux copy-on-write makes the initial fork cheap, but the steady-state cost is real and it scales linearly with connection count.</p>
<p>The popular "10 MB per connection" figure is a Heroku-style rule of thumb, not a constant. Andres Freund, a Postgres committer, did the careful measurement in 2020 and found a wider range. An idle connection without <code>huge_pages</code> shows around 16 MiB RSS, but the true Proportional Set Size overhead is closer to 7.6 MiB. With <code>huge_pages=on</code>, the true overhead drops to about 1.3 MiB. AWS measured idle connections on RDS Postgres at about 1.5 MB, climbing to 10.8 MB after a single SELECT and 14.5 MB after a query that touches temp tables. The right way to think about it is "1 to 15 MB per connection depending on what the client just did and how the kernel is configured", not a flat number.</p>
<p>Memory is the easy part. The harder problem is CPU and snapshot scalability. Pre-Postgres-14, every transaction called <code>GetSnapshotData()</code> which scanned the entire process array. Andres Freund and the Citus team measured this with pgbench on Postgres 12. With one active connection and zero idle, they hit 33,457 TPS. Add 10,000 idle connections and the same workload dropped to 14,496 TPS, a 57% loss. CPU profiling showed half the time in <code>GetSnapshotData()</code>. Postgres 14 fixed most of this and roughly doubled throughput at high idle counts, but the overhead is still real, and plenty of teams are running on managed Postgres versions that are pinned to older majors.</p>
<p>There is one more multiplier that catches people off guard. The <code>work_mem</code> setting is per operation, not per connection. The official docs put it plainly: a complex query might run several sort and hash operations at the same time, with each one allowed to use <code>work_mem</code> before spilling to disk. The default is 4 MB, but a query with three hash joins on a single backend can pull 12 MB on its own. Multiply that by 500 backends doing real work and you understand why staging melted.</p>
<p>That is why <code>max_connections</code> defaults to 100. The Postgres wiki on <a href="https://wiki.postgresql.org/wiki/Number_Of_Database_Connections">Number Of Database Connections</a> says it directly: you can often support more concurrent users by reducing the number of database connections and using some form of connection pooling. Raising <code>max_connections</code> is rarely the right move, because Postgres pre-allocates shared memory and other resources at startup based on it. You make every connection slower in exchange for being able to open more of them, which is the opposite of what you want.</p>
<h2 id="how-big-should-your-postgres-connection-pool-actually-be">How big should your Postgres connection pool actually be?</h2>
<p>The answer is "much smaller than you think". The HikariCP wiki and the PostgreSQL wiki both cite the same formula:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>connections = ((core_count * 2) + effective_spindle_count)</span></span></code></pre></figure>
<p>This number originally came from Oracle's Real-World Performance group, who found that shrinking pool size dropped response time from around 100 ms to around 2 ms in their tests, roughly a 50x improvement. Brett Wooldridge, the author of HikariCP, distilled it into one line that I now repeat every chance I get: "You want a small pool, saturated with threads waiting for connections."</p>
<p>The "effective spindle count" part needs a 2026 translation. It used to mean the number of physical disks, because spinning rust let you do real parallel I/O while one disk was seeking. On modern storage the number is closer to zero. NVMe with a hot working set in the buffer cache is effectively zero spindles. Cloud-attached SSDs like AWS gp3, GCP pd-ssd, or Azure Premium SSD v2 behave like one or two spindles under bursty load. Cold scans on cheap storage (think gp2 with the burst budget exhausted) start to look more like the old spindle model where you can usefully overlap I/O wait with CPU work.</p>
<p>Let me work through my exact incident numbers because they make the math vivid. I had a 4-core Postgres box, ten app pods, and Hikari pool size set to 500 per pod. Theoretical maximum client connections from the app fleet: 10 pods times 500 connections, equals 5,000. Postgres <code>max_connections</code> was the default 100. Even if we had raised it to 500, we would have been ten times over. The HikariCP formula for that 4-core box with a hot dataset on NVMe gives <code>(4 * 2) + 0 = 8</code> useful active backends across the entire fleet. That is less than one active backend per pod.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/postgres-connection-pool-pgbouncer-survival-guide-fork-bomb.webp" alt="A diagram showing thousands of client connections from three app pods overwhelming a 4-core Postgres database, illustrating the fork-bomb pattern" width="1600" height="845"></p>
<p>The fix was not "raise max_connections", it was the opposite. Each pod kept a small Hikari pool of 5 to 10 connections pointed at PgBouncer on port 6432. PgBouncer multiplexed those 5,000 client connection slots onto roughly 25 real Postgres backends. We did not lose throughput because the bottleneck was never the number of physical connections, it was the cost of having too many of them.</p>
<p>Little's Law is the math behind Wooldridge's axiom. <code>L = lambda * W</code>, where L is concurrent in-flight requests, lambda is arrival rate, and W is service time. If your database serves a query in 5 ms and you need 10,000 queries per second, you need <code>10,000 * 0.005 = 50</code> concurrent connections. Adding more does not increase throughput. It adds queueing inside Postgres (lock contention, context switching) which increases W, which increases required L, and you spiral into a queue that grows faster than it drains. Dan Slimmon has a nice <a href="https://blog.danslimmon.com/2022/06/07/using-littles-law-to-scale-applications/">write-up applying Little's Law to scaling decisions</a> that I send to anyone who wants to argue with the formula.</p>
<p>Heroku publishes a useful operational rule of thumb. When their pooler connects to a Postgres instance, it can open up to 75% of the database plan's connection limit, leaving 25% for direct connections from the app and admin tools. That ratio is a sane default to copy. If <code>max_connections</code> is 100, set PgBouncer's <code>max_db_connections</code> to 75 or so, and reserve the rest for psql sessions and migration jobs.</p>
<h2 id="what-is-pgbouncer-and-how-does-transaction-mode-work">What is PgBouncer and how does transaction mode work?</h2>
<p>PgBouncer is a single-process, asynchronous, lightweight connection pooler that sits between your application and Postgres and pretends to be Postgres on its end. Your app talks to it on port 6432 (the convention) and it speaks the Postgres wire protocol back to a small number of real backends. The PgBouncer features doc claims around 2 KB of memory per client connection, which is what makes the math work: you can serve thousands of clients on a single core because each one is essentially just a small allocation and an epoll registration.</p>
<p>PgBouncer ships three pool modes. The verbatim definitions from <a href="https://www.pgbouncer.org/config.html">pgbouncer.org/config.html</a> are:</p>
<table>
<thead>
<tr>
<th>Mode</th>
<th>When the backend is released back to the pool</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>session</code></td>
<td>After the client disconnects (the default).</td>
</tr>
<tr>
<td><code>transaction</code></td>
<td>After the transaction finishes (COMMIT or ROLLBACK).</td>
</tr>
<tr>
<td><code>statement</code></td>
<td>After the query finishes. Multi-statement transactions are disallowed.</td>
</tr>
</tbody>
</table>
<p>Transaction mode is the sweet spot for almost every web or microservice workload. If your transactions are short (and they should be, you are not holding a transaction open across an HTTP request boundary, right?) then a single backend can serve many clients per second. With BEGIN to COMMIT lasting 5 ms and a 25-backend pool, you can handle on the order of 5,000 transactions per second on that pool, which is more than most apps will ever need.</p>
<p>Statement mode is too restrictive for anything that uses multi-statement transactions, which means it is too restrictive for anything that uses an ORM. Session mode gives you full Postgres compatibility but throws away the multiplexing benefit, so you might as well not use a pooler. Transaction mode is where the magic happens, and it is also where you have to know what you are doing, which is the next section.</p>
<p>Version compatibility matters more than people think. PgBouncer 1.21, released October 2023, added protocol-level prepared statement support to transaction mode. That single feature unblocked a huge class of applications that previously had to disable client-side prepared statements (with <code>prepareThreshold=0</code> in JDBC, for example) to use a pooler. Version 1.24, January 2025, made prepared statement support default-on with <code>max_prepared_statements = 200</code>. The current stable as of April 2026 is 1.25.1, released December 2025, which fixes CVE-2025-12819 (an unauthenticated SQL execution via a malicious <code>search_path</code> in the StartupMessage) and a SCRAM auth regression. If you are running anything older than 1.25.1, upgrade.</p>
<p>PgBouncer is single-threaded by design. To use more than one CPU core on a busy host, you run multiple PgBouncer processes bound to the same port via <code>SO_REUSEPORT</code>, and the Linux kernel distributes incoming connections across them. To keep query cancellation working correctly across processes, configure the <code>peers</code> section so any process can route a cancel message to the one holding the original connection. Crunchy Data has <a href="https://www.crunchydata.com/blog/postgres-at-scale-running-multiple-pgbouncers">a good writeup on running multiple PgBouncers at scale</a>, and Zalando's engineering blog covers their central-deployment pattern with dedicated CPU cores.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/postgres-connection-pool-pgbouncer-survival-guide-pool-modes.webp" alt="A side-by-side diagram of session, transaction, and statement pool modes showing where each releases the backend connection" width="1600" height="845"></p>
<h2 id="which-postgres-features-break-in-pgbouncer-transaction-mode">Which Postgres features break in PgBouncer transaction mode?</h2>
<p>Transaction mode breaks anything that depends on session-scoped state, because the next transaction from the same client may be served by a completely different backend. The list is finite and well-documented, but every team I have worked with has been bitten by at least one of these.</p>
<p>Server-side prepared statements were the historical pain point. Before PgBouncer 1.21, when a client called <code>PREPARE foo</code> and then sent another <code>EXECUTE foo</code> in a new transaction, the second call could land on a backend that had never seen <code>foo</code>, and you would get a <code>prepared statement does not exist</code> error in production at 3 a.m. The standard JDBC workaround was <code>prepareThreshold=0</code> in the connection string, which disabled client-side prepared statement caching entirely. PHP teams set <code>PDO::ATTR_EMULATE_PREPARES = true</code> for the same reason. Both workarounds gave up the planning-time savings of prepared statements, which on complex queries is a big deal. Crunchy measured a 15-table self-join going from 174.896 ms of planning time without prepared statements to 0.020 ms with them, roughly an 8,700x speedup.</p>
<p>PgBouncer 1.21 fixed this for the protocol path. PgBouncer now intercepts the wire-protocol <code>Parse</code>, <code>Bind</code>, <code>Describe</code>, and <code>Execute</code> messages, renames each prepared statement to its own internal name like <code>PGBOUNCER_1</code>, and re-issues the <code>Parse</code> on whichever backend it routes the next transaction to. Your client sees the statement working transparently. Pgjdbc, npgsql, pgx, asyncpg, and psycopg3 all use this protocol path, so they get the speedup automatically once you enable <code>max_prepared_statements</code>.</p>
<p>Here is the gotcha that catches people: SQL-level <code>PREPARE foo AS SELECT ...</code> is not intercepted, only protocol-level prepared statements are. If your code literally sends the text <code>PREPARE</code> to the database, PgBouncer treats it like any other SQL and the next transaction will not see it. This caveat is on the <a href="https://www.pgbouncer.org/faq.html">PgBouncer FAQ</a> but easy to miss.</p>
<p><code>SET</code> and <code>SET LOCAL</code> behave differently in transaction mode. <code>SET LOCAL statement_timeout = '5s'</code> is transaction-scoped and works fine because it dies at COMMIT. <code>SET statement_timeout = '5s'</code> is session-scoped and breaks, because the next transaction may land on a different backend that does not have the GUC set. The fix is to either use <code>SET LOCAL</code>, or pass settings via the connection string with <code>options=-c statement_timeout=5s</code>, which applies them at backend startup.</p>
<p><code>LISTEN</code> and <code>NOTIFY</code> are flat-out broken in transaction mode. <code>LISTEN</code> registers a session-scoped subscription on a backend you no longer own after the transaction ends. The right answer is to keep a separate non-pooled connection (going directly to Postgres on port 5432) for any worker that needs to listen for notifications. Heroku's docs make this explicit and I have seen the same advice in every production-grade pooling guide.</p>
<p>Advisory locks split into two categories. <code>pg_advisory_lock</code> is session-scoped and broken in transaction mode for the same reason <code>LISTEN</code> is. <code>pg_advisory_xact_lock</code> is transaction-scoped and works fine because it releases at COMMIT or ROLLBACK. Rails users, in particular, have to set <code>advisory_locks: false</code> in <code>database.yml</code> when using PgBouncer transaction mode, because Rails uses session-scoped advisory locks for migration coordination by default.</p>
<p><code>WITH HOLD</code> cursors are designed to outlive a transaction, which is the exact thing transaction mode does not support. The next transaction may not see the same backend, so the cursor is gone. The fix is to drop <code>WITH HOLD</code> and consume the cursor inside the transaction, or to use session mode for that specific connection.</p>
<p>Temp tables behave the way you would expect: <code>CREATE TEMP TABLE foo ON COMMIT DROP</code> works because the table dies with the transaction, and <code>ON COMMIT PRESERVE ROWS</code> breaks because the table is session-scoped. If your job needs a temp table that lives across transactions, you need a different pattern, often a regular table with a job ID column and a cleanup process.</p>
<h2 id="how-do-you-configure-pgbouncer-for-production">How do you configure PgBouncer for production?</h2>
<p>You configure it by changing the connection string and writing a <code>pgbouncer.ini</code> that matches your Postgres ceiling. The connection string change is the easy part. Your apps go from connecting directly to the database:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>postgres://app:pwd@db.internal:5432/myapp</span></span></code></pre></figure>
<p>To connecting through PgBouncer:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>postgres://app:pwd@pgbouncer.internal:6432/myapp</span></span></code></pre></figure>
<p>Your application code does not change. PgBouncer pretends to be Postgres, your driver does not know the difference.</p>
<p>Here is a <code>pgbouncer.ini</code> snippet that would have prevented my incident, sized for the same 4-core Postgres box:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ini" data-theme="material-theme github-light"><code data-language="ini" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[databases]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">myapp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#F07178;--shiki-light:#D73A49"> host</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">db.internal </span><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">5432 </span><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">dbname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">myapp</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#6F42C1">[pgbouncer]</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">listen_addr</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 0.0.0.0</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">listen_port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 6432</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">pool_mode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> transaction</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">max_client_conn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 5000</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">default_pool_size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 25</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">max_db_connections</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 30</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">reserve_pool_size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 5</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">reserve_pool_timeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 3.0</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">server_idle_timeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 60</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">max_prepared_statements</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 200</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">auth_type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> scram-sha-256</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">auth_file</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> /etc/pgbouncer/userlist.txt</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">admin_users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pgbadmin</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">stats_users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pgbadmin</span></span></code></pre></figure>
<p>Walking through the knobs that matter:</p>
<ul>
<li><code>pool_mode = transaction</code> is the multiplexing setting we have been talking about.</li>
<li><code>max_client_conn = 5000</code> is the ceiling on app-side connections. Set it well above your actual demand because the per-client memory is tiny.</li>
<li><code>default_pool_size = 25</code> is the number of real Postgres backends per (user, database) pair. This is the number you derive from the HikariCP formula plus a small buffer.</li>
<li><code>max_db_connections = 30</code> is the hard cap on backends to a single database, regardless of pool count. Set it under your Postgres <code>max_connections</code> minus reserved superuser slots.</li>
<li><code>reserve_pool_size = 5</code> allows five extra backends to spin up under sustained load, after the wait time below.</li>
<li><code>reserve_pool_timeout = 3.0</code> means a client that waits more than 3 seconds for a connection triggers the reserve pool.</li>
<li><code>server_idle_timeout = 60</code> recycles backends that sit idle for a minute, freeing Postgres-side memory.</li>
<li><code>max_prepared_statements = 200</code> enables protocol-level prepared statements. Default since 1.24, but set it explicitly so you can read the value off the file.</li>
</ul>
<p>Authentication should always be <code>scram-sha-256</code> in 2026. The older <code>md5</code> hash format is effectively obsolete and you will fail a security review the day someone notices it. The <code>auth_user</code> plus <code>auth_query</code> pattern lets PgBouncer look up arbitrary users from <code>pg_authid</code> on demand instead of pre-loading every user into <code>userlist.txt</code>, which is helpful when you have a large or dynamic user list. The default <code>auth_query</code> per the PgBouncer docs is:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">SELECT</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> rolname,</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">  CASE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> WHEN</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> rolvaliduntil </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> THEN</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NULL</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> ELSE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> rolpassword </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">END</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">FROM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pg_authid</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">WHERE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> rolname </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> $</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> AND</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> rolcanlogin</span></span></code></pre></figure>
<p>The monitoring story is where PgBouncer earns its keep. Connect to the special database called <code>pgbouncer</code> on port 6432 with a user listed in <code>admin_users</code> or <code>stats_users</code>, and you get a small command set that tells you everything about pool health. The four commands I run constantly are <code>SHOW POOLS</code>, <code>SHOW STATS</code>, <code>SHOW CLIENTS</code>, and <code>SHOW SERVERS</code>. A real <code>SHOW POOLS</code> row looks like this (formatted as a code block, values illustrative):</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span> database  | user | cl_active | cl_waiting | sv_active | sv_idle | sv_used | maxwait | pool_mode</span></span>
<span data-line=""><span>-----------+------+-----------+------------+-----------+---------+---------+---------+-------------</span></span>
<span data-line=""><span> myapp     | app  |       142 |          3 |        18 |       7 |       0 |    0.04 | transaction</span></span></code></pre></figure>
<p>Decoded: 142 clients are connected and busy, 3 are queued waiting for a backend, 18 backends are actively running queries, 7 are idle in the pool, the longest waiting client has been waiting 0.04 seconds. If <code>cl_waiting</code> stays above zero for sustained periods, your pool is too small. If <code>maxwait</code> climbs above one second, your users are noticing.</p>
<p>For ongoing observability, deploy the <a href="https://github.com/prometheus-community/pgbouncer_exporter">prometheus-community/pgbouncer_exporter</a> on port 9127. It scrapes <code>SHOW LISTS</code>, <code>SHOW STATS</code>, <code>SHOW POOLS</code>, and <code>SHOW DATABASES</code> on an interval and exposes them as Prometheus metrics. The three alerts I would set on day one are: <code>pgbouncer_pools_client_waiting > 0</code> for more than 30 seconds, <code>pgbouncer_pools_server_active / default_pool_size > 0.8</code> sustained, and <code>pgbouncer_stats_total_wait_time</code> rising over a 5-minute window. Heroku's empirical guidance is that connection queueing emerges past around 15,000 to 20,000 transactions per second on a single PgBouncer process, so add a second process via <code>SO_REUSEPORT</code> before you cross that threshold.</p>
<p>You will also want a small set of Postgres-side queries to diagnose what is happening on the database when PgBouncer says it is overloaded. The two I keep in my snippets file:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">-- Count active backends by state</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">SELECT</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> state</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">count</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">FROM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pg_stat_activity</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">GROUP BY</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> state</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">-- Find idle-in-transaction sessions older than 5 minutes</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">SELECT</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pid, usename, client_addr, </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">state</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">       now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> state_change </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">AS</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> state_age, query</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">FROM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pg_stat_activity</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">WHERE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> state</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">idle in transaction</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">  AND</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> state_change </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> interval </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">5 minutes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>The first one tells you whether your <code>max_connections</code> headroom is real. The second one finds the application bug that holds a transaction open across a long-running call, which is the silent killer of pool health because it pins a backend forever and cascades into queue growth.</p>
<p>For deployment, three patterns. Sidecar per pod is the simplest and works fine for small fleets. A central PgBouncer Deployment behind a Service is what Zalando does, with multiple replicas using <code>SO_REUSEPORT</code> and pinned to dedicated CPU cores so the kernel's network interrupts do not fight your application workloads for the same hyperthreads. AWS RDS Proxy is the managed option if you do not want to run any infrastructure, with the trade-off being less visibility and a more opinionated set of features.</p>
<h2 id="when-should-you-outgrow-pgbouncer">When should you outgrow PgBouncer?</h2>
<p>You outgrow PgBouncer when one of three things stops being true: the workload fits into one process, the deployment topology is simple enough that running PgBouncer as a sidecar or central deployment is fine, or you do not need query-aware features like read-replica routing or sharding. Once any of those break, you start looking at alternatives.</p>
<p>PgCat is the Rust answer. Multi-threaded by default, sharding-aware (with explicit <code>SET SHARD TO 'N'</code>, comment-based sharding, or auto-parsed via a SQL parser), and with built-in load balancing across primary and replicas. Instacart and PostgresML use it in production, with PostgresML claiming "scaling to 1 million requests per second" on their hardware. PgCat speaks the PgBouncer admin protocol, so existing monitoring tooling carries over. The latest tagged release is v1.2.0 from August 2024 with subsequent patch tags into November 2024, and the release cadence has slowed since then. It is still actively used in production but worth tracking the project before adopting.</p>
<p>Supavisor is Supabase's pooler, written in Elixir on the BEAM. Its design point is multi-tenant fan-out: it can hold millions of lightweight client slots in BEAM scheduler memory and map them onto a small pool of real Postgres connections. Supabase has demoed roughly one million concurrent client connections. It supports named prepared statements, query cancellation, and primary-replica routing. Supabase marks it as "Public Beta" and uses it as the default for new Supabase projects.</p>
<p>Odyssey is Yandex's multi-threaded C pooler, used in their managed Postgres service. A 2025-07-16 release added HBA file support, SCRAM channel binding, and a <code>pool_discard_query</code> option. Linux x86_64 only, which constrains where you can run it.</p>
<p>AWS RDS Proxy is the managed-cloud option. AWS claims it reduces failover time for Aurora and RDS by up to 66%. It supports transaction-style multiplexing but with AWS-specific session-pinning rules: certain SQL features (like SET, temp tables, certain transactions) force the proxy to pin a client to a backend and disable multiplexing for that session. Pricing is $0.015 per ACU-hour on Aurora Serverless v2 (8 ACU minimum) or $0.015 per vCPU-hour on Provisioned (2 vCPU minimum). It is the right answer for Lambda-heavy workloads where running a PgBouncer sidecar makes no sense.</p>
<p>One common confusion to clear up. Google Cloud SQL Auth Proxy is not a connection pooler. It is an authenticated TCP proxy that handles IAM-based authentication and TLS, and you still need PgBouncer or app-side Hikari behind it. I have seen teams treat it like RDS Proxy and wonder why their connection counts are not going down.</p>
<p>When pooling alone is no longer enough, the next floor is sharding. Notion's engineering blog has <a href="https://www.notion.com/blog/sharding-postgres-at-notion">two excellent posts</a> on their sharding journey, including the <a href="https://www.notion.com/blog/the-great-re-shard">great re-shard from 32 to 96 physical Postgres databases</a> backing 480 logical shards, all fronted by PgBouncer. Their trigger was VACUUM stalls and TXID wraparound risk, not pool exhaustion specifically, but the topology is what most large Postgres deployments end up with: a wide fleet of physical databases, application-level sharding, and a pooler in front of each shard.</p>
<p>I will not invent benchmark numbers comparing these. Public head-to-head benchmarks under controlled conditions are limited and the workload-sensitivity is high. The honest answer is that for most teams, PgBouncer transaction mode on the latest stable is plenty, and the question of "which alternative" is a problem you will have when the metrics tell you it is.</p>
<h2 id="what-do-i-wish-past-me-had-known">What do I wish past-me had known?</h2>
<p>Pool size is a load-bearing config. Default to small, saturate with waiters, and add a pooler before you think you need one. The "make the number bigger" instinct is exactly backwards for Postgres connections, because the cost of a connection is mostly fixed and the benefit of more of them tops out faster than you expect. The HikariCP formula is not the final word, but it is a much better starting point than whatever Stack Overflow tells you, and it forces you to think about your actual hardware instead of guessing.</p>
<p>If you are running Postgres in production and have not measured your <code>pg_stat_activity</code> counts and your pool's <code>cl_waiting</code> over the last week, that is your homework for this week. Almost every team I have looked at in the last two years has been running with a pool that is either too big and quietly hurting throughput, or too small and silently queueing requests during traffic spikes. The fix is usually a one-line config change and an evening of monitoring.</p>
<p>What is your current Postgres pool size, and how did you arrive at it? I would love to hear in the comments or on the Twitter thread linked from this post.</p>
<p>For more on pool sizing, see the <a href="https://wiki.postgresql.org/wiki/Number_Of_Database_Connections">PostgreSQL wiki on Number Of Database Connections</a> and the <a href="https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing">HikariCP About Pool Sizing</a> page. For PgBouncer specifics, the <a href="https://www.pgbouncer.org/config.html">official config reference</a> and the <a href="https://www.pgbouncer.org/features.html">features matrix</a> are the only sources you should trust, and Andres Freund's <a href="https://blog.anarazel.de/2020/10/07/measuring-the-memory-overhead-of-a-postgres-connection/">Postgres connection memory measurement</a> plus the <a href="https://www.citusdata.com/blog/2020/10/08/analyzing-connection-scalability/">Citus connection scalability analysis</a> give you the empirical foundation behind every number in this post.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Hibernate Lazy Initialization: A Practical Guide</a> — Adjacent DB tuning territory: how the JPA/Hibernate side of your app affects connection lifetime and pool pressure.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">Implementing the Outbox Pattern with CDC for Microservices</a> — When pooling is solved, the next class of database problems is dual-writes and consistency across services.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide">Spring Boot Testcontainers Guide</a> — Test your pool config against a real Postgres in CI instead of an embedded H2 that lies about everything.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Vercel AI Gateway: One Key for Every LLM, With BYOK Support]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive</link>
      <guid>https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive</guid>
      <pubDate>Thu, 23 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Vercel AI Gateway unifies hundreds of AI models behind one API key with fallbacks, BYOK, and zero markup. A hands-on deep dive with AI SDK v6 code.]]></description>
      <content:encoded><![CDATA[<p>I spent most of 2024 juggling four SDKs: OpenAI, Anthropic, Google, and one for a fine-tuned Llama endpoint. Four sets of keys. Four billing dashboards. Four provider outages that each took down a feature.</p>
<p>Vercel AI Gateway replaced all four. One key. One endpoint. Forty providers behind it.</p>
<p>The short pitch is boring: proxy your LLM calls through a single URL. The reason it matters is subtle. When you route through the Gateway, model swaps become string edits, provider outages become automatic failovers, and cost tracking stops being a spreadsheet. I have been running production traffic through it for six months and I want to show you exactly how it is wired, not just the marketing version.</p>
<p>This post walks through the Gateway with AI SDK v6 code: the first request, provider routing, BYOK, observability, and pricing. I will also cover what changed in April 2026, because two of those changes are the reason I stopped recommending direct provider SDKs for new projects.</p>
<h2 id="what-is-the-vercel-ai-gateway">What is the Vercel AI Gateway?</h2>
<p>Vercel AI Gateway is a unified HTTP API that proxies requests to hundreds of AI models from different providers using a single endpoint and a single API key. You call it with the standard AI SDK, OpenAI SDK, or Anthropic SDK, and Vercel forwards the request to the right provider underneath.</p>
<p>The base URL is <code>https://ai-gateway.vercel.sh/v1</code>. Authentication is an <code>AI_GATEWAY_API_KEY</code> bearer token, or Vercel OIDC when you deploy the same app to Vercel.</p>
<p>The model string format is <code>provider/model</code>. So <code>anthropic/claude-opus-4.6</code>, <code>openai/gpt-5.4</code>, <code>google/gemini-3.1-pro</code>, <code>xai/grok-4.1-fast-non-reasoning</code>. You change provider by editing the string. No new SDK install. No new env var. That is the whole idea.</p>
<p>Under the hood, Vercel maintains system credentials with each provider, pools them for reliability, and picks which provider handles your request based on uptime, latency, and the preferences you set. If one provider is slow or down, the Gateway retries against another that can serve the same model. For models like Claude that exist on multiple clouds (direct from Anthropic, Bedrock, Vertex), you can route to any of them without changing the code.</p>
<p>The provider list as of April 2026 covers 40+ organizations: Alibaba, Anthropic, Azure, Baseten, Amazon Bedrock, Black Forest Labs, ByteDance, Cerebras, Cohere, DeepInfra, DeepSeek, Fireworks, Google, Groq, Inception, Kling AI, MiniMax, Mistral, Moonshot AI, Novita, OpenAI, Perplexity, Recraft, SambaNova, Together AI, Vercel, Google Vertex AI, Voyage AI, xAI, Z.ai, and more. New ones show up roughly every week.</p>
<p>One important nuance: AI Gateway is not a new inference layer. Your traffic still ends up at OpenAI, Anthropic, or wherever. Vercel sits in the middle, adds routing and observability, and charges you the exact provider list price with no markup.</p>
<h2 id="why-would-i-use-the-ai-gateway-instead-of-calling-providers-directly">Why would I use the AI Gateway instead of calling providers directly?</h2>
<p>You use the AI Gateway when you want model portability, automatic failover, and unified billing without building that infrastructure yourself. You skip it when you need a niche provider feature that is not yet surfaced through the Gateway.</p>
<p>Here are the real benefits I have measured on my own projects.</p>
<h3 id="model-swaps-become-one-line-edits">Model swaps become one-line edits</h3>
<p>Before: to move a prompt from Claude Opus to GPT-5.4, I had to swap packages, rewrite the client init, change the messages shape, change how I extracted the response text, and sometimes rewrite the streaming logic. Maybe 15-40 lines of diff per swap.</p>
<p>After: the model string changes. Everything else stays.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Was Claude</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> streamText</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic/claude-opus-4.6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Now GPT-5.4</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> streamText</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">openai/gpt-5.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>That is the whole change. The streaming API, the token usage API, the error shapes, the tool-calling format, all normalized by the AI SDK on top of the Gateway.</p>
<h3 id="automatic-provider-failover">Automatic provider failover</h3>
<p>Every LLM provider has outages. OpenAI has had them. Anthropic has had them. Groq has had them. When your app depends on one provider, their outage is your outage.</p>
<p>With the Gateway, the fallback is built in. For models hosted by multiple providers (Claude via Anthropic and Bedrock, Llama via Groq and Together, Gemini via Google and Vertex), you set a preference order and the Gateway handles the rest. If provider A times out or errors, provider B takes the request.</p>
<h3 id="one-bill-one-dashboard">One bill, one dashboard</h3>
<p>The spend view in the Gateway dashboard shows total cost, cost by model, cost by project, and cost by API key. Four providers, one invoice. I used to spend the last Monday of every month reconciling four billing pages. Now I look at one number.</p>
<h3 id="zero-markup-pricing">Zero markup pricing</h3>
<p>This is the one that always surprises people. Vercel does not add a fee on top of provider token prices. The free tier is $5 per month (starts when you first call the Gateway), the paid tier is pay-as-you-go at provider list price. Even BYOK is zero markup, because your tokens go through your own provider contract.</p>
<p>The obvious catch: you still need a positive AI Gateway credits balance because if your BYOK credentials fail, the Gateway falls back to its system credentials and charges against your Vercel balance. That fallback is a feature, not a trap.</p>
<h2 id="how-do-i-make-my-first-request-through-the-ai-gateway">How do I make my first request through the AI Gateway?</h2>
<p>You make your first request by installing the <code>ai</code> package, setting <code>AI_GATEWAY_API_KEY</code> in your environment, and calling <code>streamText</code> or <code>generateText</code> with a <code>'provider/model'</code> string.</p>
<p>Here is the shortest useful example for a Next.js App Router API route.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">pnpm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> add</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ai</span></span></code></pre></figure>
<p>Add your key to <code>.env.local</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">AI_GATEWAY_API_KEY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">vck_your_key_here</span></span></code></pre></figure>
<p>Then a basic streaming route at <code>app/api/chat/route.ts</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> streamText</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> streamText</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">openai/gpt-5.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toUIMessageStreamResponse</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That is it. No client init, no base URL, no provider package. The AI SDK detects <code>AI_GATEWAY_API_KEY</code> and hits <code>https://ai-gateway.vercel.sh/v1</code> on your behalf.</p>
<p>For a non-streaming call:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> generateText</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> usage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> generateText</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic/claude-opus-4.6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> usage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/vercel-ai-gateway-deep-dive-routing.webp" alt="Request flow from app through AI Gateway to the selected provider" width="1600" height="845"></p>
<h3 id="using-the-openai-or-anthropic-sdks-directly">Using the OpenAI or Anthropic SDKs directly</h3>
<p>If you already have code written against the OpenAI or Anthropic TypeScript SDKs, you do not need to switch to the AI SDK. You can point those SDKs at the Gateway and keep shipping:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> OpenAI </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">openai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> OpenAI</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  apiKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> process</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">env</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">AI_GATEWAY_API_KEY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  baseURL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://ai-gateway.vercel.sh/v1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">chat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">completions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic/claude-opus-4.6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Write a haiku about QUIC.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Notice the model: the OpenAI SDK is talking to Claude through the Gateway. Any model string from the Gateway's catalog works, regardless of which SDK sent the request. The Anthropic SDK works the same way: set <code>baseURL: 'https://ai-gateway.vercel.sh'</code> and it is routed.</p>
<h3 id="deploying-on-vercel-gets-oidc-for-free">Deploying on Vercel gets OIDC for free</h3>
<p>When your app is deployed on Vercel, the AI SDK automatically picks up a short-lived OIDC token instead of needing <code>AI_GATEWAY_API_KEY</code>. That means you can remove the key from production environment variables and let Vercel handle authentication between your project and the Gateway. In dev you still use the key. I leave it in <code>.env.local</code> and delete it from the Vercel project settings once the preview deploys work.</p>
<h2 id="how-do-i-pick-which-provider-handles-a-model">How do I pick which provider handles a model?</h2>
<p>You pick providers with <code>providerOptions.gateway</code> using <code>order</code>, <code>only</code>, or <code>sort</code>. Each controls routing in a different way, and you can combine them.</p>
<h3 id="order-a-preference-list">order: a preference list</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> streamText</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic/claude-opus-4.6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  providerOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bedrock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>This says: try Bedrock first. If Bedrock is down or returns an error, try Anthropic. You get the same Claude model either way, but the first-choice provider dictates pricing and latency. I use this when a model is cheaper on one provider but less reliable.</p>
<h3 id="only-a-hard-allowlist">only: a hard allowlist</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">providerOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">  gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    only</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bedrock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span></code></pre></figure>
<p><code>only</code> restricts the set of providers the Gateway may use. Other providers that also serve this model are excluded entirely. This matters for compliance: if your contract says Claude traffic goes through Anthropic directly or through AWS Bedrock but not through Vertex, <code>only</code> enforces that.</p>
<h3 id="sort-rank-by-cost-latency-or-throughput">sort: rank by cost, latency, or throughput</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">providerOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">  gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">    sort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">cost</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // or 'ttft' for time to first token, 'tps' for tokens per second</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span></code></pre></figure>
<p><code>sort</code> picks a provider based on a metric. <code>cost</code> is self-explanatory. <code>ttft</code> optimizes for snappy feel, which is what you want for chat UIs. <code>tps</code> optimizes for total throughput, which is what you want for batch jobs. The Gateway ranks the candidate providers by the metric and tries them in order.</p>
<h3 id="combining-them">Combining them</h3>
<p>Nothing stops you from combining these with provider-specific options at the same time. Here is a realistic production snippet:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> streamText</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> streamText</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic/claude-opus-4.6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    providerOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      anthropic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        thinkingBudget</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0.001</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      },</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">bedrock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        sort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ttft</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        caching</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">auto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toUIMessageStreamResponse</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This says: use Claude Opus 4.6, with an extended-thinking budget of $0.001 per request, preferring Bedrock over Anthropic, and falling back to whichever is fastest to first token. Let the Gateway apply provider-appropriate caching automatically.</p>
<p>The <code>caching: 'auto'</code> flag is worth a paragraph on its own. Anthropic and a couple other providers require explicit cache markers to benefit from prompt caching. Adding them correctly is easy to get wrong and changes per provider. <code>auto</code> makes the Gateway insert the right markers per provider, which on a big system prompt can cut cost by 50-80%. I turn it on by default.</p>
<h2 id="how-does-bring-your-own-key-work-with-the-gateway">How does Bring Your Own Key work with the Gateway?</h2>
<p>BYOK lets you route Gateway requests through your own provider credentials instead of Vercel's pooled system credentials, with zero markup on tokens and automatic fallback if your key fails.</p>
<p>The two reasons to use BYOK are: you have provider credits you want to burn, or you have a private-network or regional requirement that only your own account satisfies.</p>
<h3 id="team-level-byok">Team-level BYOK</h3>
<p>The default setup is in the dashboard. Go to AI Gateway, Bring Your Own Key, pick a provider, paste your key, test it, enable it. Done. Every request for that provider now uses your key. If your key fails, the Gateway retries with system credentials and charges your AI Gateway balance.</p>
<h3 id="request-scoped-byok">Request-scoped BYOK</h3>
<p>For more granular control, pass credentials per request:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> GatewayProviderOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@ai-sdk/gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> generateText</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> generateText</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">anthropic/claude-opus-4.6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Write a limerick about QUIC.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  providerOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    gateway</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">      byok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">        anthropic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> apiKey</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> process</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">env</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">CUSTOMER_ANTHROPIC_KEY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> satisfies</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> GatewayProviderOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Each provider has a different credential shape.</p>
<table>
<thead>
<tr>
<th>Provider</th>
<th>Credential</th>
</tr>
</thead>
<tbody>
<tr>
<td>Anthropic</td>
<td><code>{ apiKey }</code></td>
</tr>
<tr>
<td>OpenAI</td>
<td><code>{ apiKey }</code></td>
</tr>
<tr>
<td>Azure</td>
<td><code>{ apiKey, resourceName }</code></td>
</tr>
<tr>
<td>Vertex</td>
<td><code>{ project, location, googleCredentials: { privateKey, clientEmail } }</code></td>
</tr>
<tr>
<td>Bedrock</td>
<td><code>{ accessKeyId, secretAccessKey, region? }</code></td>
</tr>
</tbody>
</table>
<p>You can pass multiple credentials per provider, and the Gateway tries them in order. This is how you do per-tenant isolation in a multi-customer app: each tenant gets their own key, their usage gets billed to their account, and the Gateway still handles failover.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/vercel-ai-gateway-deep-dive-byok.webp" alt="BYOK request flow with fallback to system credentials on failure" width="1600" height="845"></p>
<h3 id="why-it-matters">Why it matters</h3>
<p>The BYOK design is the first one I have seen that does not punish you for bringing your own key. Most proxies charge a "markup" on BYOK requests because they think you should pay for their infrastructure. Vercel charges zero. The only balance you need is a small AI Gateway credits cushion to cover the fallback path if your credentials fail. That is it.</p>
<p>I pair this with <a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">AI Gateway for Claude Code subscribers</a> for internal tooling, where I want traceability but also want to keep using my own Anthropic contract.</p>
<h2 id="what-does-observability-look-like-in-the-gateway-dashboard">What does observability look like in the Gateway dashboard?</h2>
<p>The Gateway dashboard gives you four metrics out of the box: requests by model, time to first token, input and output token counts, and spend, plus a detailed request log with filters for project, API key, and time range.</p>
<p>The metrics I actually watch:</p>
<p><strong>Requests by model</strong> tells me which models are getting traffic. When I roll out a new model behind a feature flag, this chart confirms the flag is actually routing traffic to it.</p>
<p><strong>Time to first token (TTFT)</strong> is the single most important latency metric for chat UIs. Users do not care about total completion time, they care about how long they stare at an empty chat before text starts appearing. When TTFT drifts up, I check the provider routing and sometimes add a <code>sort: 'ttft'</code> hint.</p>
<p><strong>Input and output token counts</strong> catch the classic failure mode where a prompt accidentally includes an entire document. I have caught two bugs this way that were silently costing me 10x expected tokens.</p>
<p><strong>Spend</strong> is self-explanatory. The chart shows spend over time, broken down by model. The first time I saw Claude Opus spend pass my Sonnet spend I knew I had a prompt-routing bug.</p>
<h3 id="request-log">Request log</h3>
<p>The request log is the debug tool. Every request is logged with full metadata: model, provider, token count, cost, duration, TTFT, project, API key, error if any. You can filter, sort, and export.</p>
<p>When a user reports a hallucination or a tool-calling failure, I grab the request ID from client logs, paste it into the Gateway log filter, and see exactly which provider handled it, how long it took, and what the token usage was. That three-minute workflow used to be a two-hour archaeology dig across four provider dashboards.</p>
<h3 id="retention-and-deeper-dashboards">Retention and deeper dashboards</h3>
<p>The default retention is limited. For longer history and deeper dashboards, you need Vercel's Observability Plus add-on. I have not needed it for my projects yet, but for anyone running production traffic at scale, it is the right move.</p>
<h2 id="how-much-does-the-ai-gateway-cost">How much does the AI Gateway cost?</h2>
<p>The AI Gateway costs exactly what the provider charges, with no markup from Vercel. Free tier is $5 per month in credits, paid tier is pay-as-you-go, and BYOK has zero markup too.</p>
<p>The exact pricing model:</p>
<ol>
<li>New Vercel team accounts get $5 per month in AI Gateway credits. This is a monthly allowance and does not accumulate.</li>
<li>Once you buy any credits, you are on the paid tier. The monthly free $5 stops. Paid credits do not expire.</li>
<li>Every request deducts from your balance at the provider's list price. You can see per-model pricing in the Gateway dashboard and at <code>vercel.com/ai-gateway/models</code>.</li>
<li>BYOK requests charge your provider account directly, not Vercel. But if your BYOK credentials fail, the fallback to Vercel system credentials gets billed against your Gateway balance.</li>
</ol>
<p>What is not included: payment processing fees may apply on top-ups. Observability Plus is a separate add-on if you need it.</p>
<h3 id="what-does-this-actually-look-like-in-practice">What does this actually look like in practice?</h3>
<p>I run a mid-sized Next.js app with an AI chat feature. Volume is around 8,000 chat messages per day, mostly Claude Sonnet with some Opus for hard questions. My average monthly Gateway bill is roughly $180. That is exactly what Anthropic would have charged me direct. The Gateway added zero dollars on top.</p>
<p>Compare that to the two paid "AI gateway" products I used in 2024: one charged a 15% markup, the other charged a flat $99/month plus usage. Vercel's pricing model is the reason I moved my traffic.</p>
<h3 id="what-you-are-paying-for">What you are paying for</h3>
<p>You are not paying Vercel for tokens. You are paying them for the routing, the failover, the observability, the dashboard, and the unified billing. Their model is: make the free tier generous enough that small apps just use it, and let bigger apps pay for tokens at cost while Vercel up-sells Observability Plus and other infrastructure pieces. If you ship on Vercel anyway, the Gateway adds nothing to the bill you were already going to pay.</p>
<h2 id="what-changed-in-the-vercel-ai-gateway-in-april-2026">What changed in the Vercel AI Gateway in April 2026?</h2>
<p>April 2026 brought three updates worth calling out: team-wide Zero Data Retention routing, the addition of Qwen 3.6 Plus, and the launch of ByteDance's Seedance 2.0 video model through the Gateway.</p>
<h3 id="team-wide-zero-data-retention">Team-wide Zero Data Retention</h3>
<p>The April 6, 2026 changelog added a team-level toggle for Zero Data Retention. When it is on, the Gateway only routes requests to providers that have a ZDR agreement with Vercel. Anthropic, OpenAI, Google, and more are covered as of April. If you flip it on and then try to route to a provider without ZDR, the request is rejected at the Gateway layer before it touches the provider.</p>
<p>For anyone dealing with regulated data, this is the setting that makes the Gateway viable. Before this you had to enforce ZDR per request or per provider manually. Now it is a team-level switch. I turned it on for a healthcare-adjacent client the day it shipped.</p>
<h3 id="qwen-36-plus">Qwen 3.6 Plus</h3>
<p>Qwen 3.6 Plus landed mid-April with a 1M context window, stronger agentic coding, and better tool calling. It is available via Alibaba Cloud through the Gateway. The 1M context is the headline, but in my quick testing the agentic coding was the real win: it picks up on structured instruction better than Qwen 3.5 did.</p>
<p>To try it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> streamText</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">alibaba/qwen-3.6-plus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<h3 id="seedance-20-video">Seedance 2.0 video</h3>
<p>Vercel added Seedance 2.0 from ByteDance as a Gateway-accessible video model. No separate provider account required. For anyone who has spent a weekend trying to get access to a video generation API, that is a meaningful quality-of-life improvement.</p>
<p>These three changes are the reason I moved the AI Gateway from "interesting" to "default" for new projects in April.</p>
<h2 id="when-should-i-not-use-the-vercel-ai-gateway">When should I not use the Vercel AI Gateway?</h2>
<p>Skip the Gateway when you need a niche provider feature it has not surfaced yet, when you have a private-network requirement that blocks proxying, or when your app runs entirely outside the HTTP-gateway cost envelope.</p>
<p>Concrete cases I have run into.</p>
<p><strong>A provider feature is too new.</strong> If OpenAI shipped something this morning, it can take the Gateway a few days to surface it. If you need bleeding-edge access, go direct until the Gateway catches up.</p>
<p><strong>You are running inside a VPC with private provider endpoints.</strong> Some enterprises route provider traffic over private links. The Gateway is a public HTTPS proxy. For private VPC setups, you cannot route through it.</p>
<p><strong>You are doing millions of requests per second.</strong> At that scale, the routing overhead (still a few ms) matters, and direct provider connections plus your own load balancer might be cheaper in engineering time than paying someone else's middleware tax. But this is a tiny fraction of teams. Almost everyone reading this is not at that scale.</p>
<p><strong>You have a strong single-provider contract.</strong> If you have negotiated a volume discount with one provider and you are not using multiple, the Gateway's unified-billing feature has less value. You still get observability and the BYOK fallback, which are worth something, but the cost-arbitrage story goes away.</p>
<p>For literally everyone else building LLM features on top of Node, Next.js, or just HTTPS: the Gateway is the default. Start there, measure, and only pull out if a specific constraint forces it.</p>
<h2 id="where-does-this-leave-the-ai-stack">Where does this leave the AI stack?</h2>
<p>The AI Gateway is part of a broader pattern I have been watching: infrastructure providers taking over the integration layer between application code and model providers. Cloudflare's Workers AI does this. AWS Bedrock does this. Vercel's Gateway is the one I like best because the SDK integration is tight and the pricing is honest.</p>
<p>Pair the Gateway with the AI SDK v6's tool calling and streaming primitives, wire in AI Gateway observability, and you get a production-ready LLM stack in a weekend. No glue code. No three-tier billing spreadsheet. No "which SDK am I using today" confusion.</p>
<p>That is the actual shift, and it is a big one. The API layer between your app and the models is standardizing. One endpoint, one shape, many providers. HTTP for the next era of software, finally.</p>
<p>For more on the Vercel AI Gateway, see the <a href="https://vercel.com/docs/ai-gateway">official AI Gateway documentation</a>, the <a href="https://vercel.com/docs/ai-gateway/models-and-providers/provider-options">provider options reference</a>, and the <a href="https://ai-sdk.dev/docs">AI SDK documentation</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Claude Opus 4.7 API Pricing, Benchmarks &#x26; Breaking Changes 2026</a>. The provider-side context for the <code>anthropic/claude-opus-4.7</code> model string you route through the Gateway, including API pricing per million tokens and the four breaking changes since Opus 4.6.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects: Which Abstraction Wins?</a>. The other half of the AI-for-developers story: what to build on top of a model, not just how to call one.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/google-antigravity">Google Antigravity: The New AI Stack</a>. How the hyperscalers are positioning their AI infrastructure, and why it matters for your model choices.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">Hello Proxy: TypeScript Proxy in Next.js 16</a>. If you are moving middleware to the new Proxy convention, this pairs well with routing API requests through the Gateway.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Java 26 HTTP/3 in the HttpClient: A Complete Developer Guide]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/java-26-http-3-httpclient</link>
      <guid>https://www.rabinarayanpatra.com/blogs/java-26-http-3-httpclient</guid>
      <pubDate>Tue, 21 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Java 26 HTTP/3 ships in the built-in HttpClient via JEP 517. Learn the opt-in API, Http3DiscoveryMode, fallback behavior, and HTTP/2 benchmarks.]]></description>
      <content:encoded><![CDATA[<p>Java 26 put HTTP/3 inside the standard library. No Netty. No extra dependency. Just <code>HttpClient.newBuilder().version(HTTP_3)</code> and you are running QUIC over UDP.</p>
<p>I have been watching this JEP since it was proposed. The JDK has had <code>java.net.http.HttpClient</code> since Java 11, but it only ever spoke HTTP/1.1 and HTTP/2. If you wanted HTTP/3 on the JVM, you reached for Netty or an experimental build of Apache HttpClient 5. That is a lot of ceremony for a protocol switch.</p>
<p>Java 26 reached general availability on 17 March 2026, and JEP 517 (HTTP/3 for the HTTP Client API) is part of that release. The pull request that landed it is reportedly the largest OpenJDK merge in recent memory. What you get is a full client-side QUIC and HTTP/3 stack inside the JDK, wired into the same <code>HttpClient</code> you already know.</p>
<p>This post is a hands-on tour. I will walk you through the first HTTP/3 request, all three discovery modes, fallback rules, a working benchmark against HTTP/2, and the sharp edges I hit while testing it on a real server.</p>
<h2 id="what-is-http3-in-java-26">What is HTTP/3 in Java 26?</h2>
<p>Java 26 HTTP/3 is a final, non-preview feature in the <code>java.net.http</code> package that lets the built-in <code>HttpClient</code> speak HTTP/3 over QUIC. You opt in per client or per request with <code>HttpClient.Version.HTTP_3</code>.</p>
<p>The short version: HTTP/3 is HTTP mapped onto QUIC instead of TCP. QUIC is a UDP-based transport that Google originally shipped for internal use in 2012, and the IETF standardized it as RFC 9000 in 2021. Everything useful about HTTP/3 is a side effect of moving off TCP.</p>
<p>Streams are multiplexed at the transport layer, so a single packet loss does not stall every in-flight request. TCP has no idea there are streams above it, so one dropped segment blocks the whole connection. QUIC knows about streams and only blocks the affected one.</p>
<p>The handshake is faster. QUIC combines TLS 1.3 and the transport handshake into a single round trip for a new connection, and zero round trips for a previously contacted server (0-RTT).</p>
<p>The connection is not tied to a network interface. QUIC uses a connection ID instead of the classic four-tuple of IPs and ports, so a mobile device moving from Wi-Fi to cellular keeps the same session with no reconnect.</p>
<p>HTTP/2 gave us stream multiplexing at the application layer, which helped. But the TCP layer underneath is still a single byte stream, and TCP head-of-line blocking is real. If you have ten concurrent HTTP/2 streams over one TCP connection and one segment gets lost, all ten streams wait for retransmission. With HTTP/3 over QUIC, only the stream that lost a packet waits.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/java-26-http-3-httpclient-hol-blocking.webp" alt="Head-of-line blocking comparison between HTTP/2 over TCP and HTTP/3 over QUIC" width="1600" height="845"></p>
<p>The reason JEP 517 is a big deal is not that HTTP/3 is new. Cloudflare, Fastly, Akamai, and every major CDN have been speaking it for years. The deal is that your Java service can finally be a first-class HTTP/3 client with zero extra dependencies.</p>
<h2 id="how-do-i-make-my-first-http3-request-in-java-26">How do I make my first HTTP/3 request in Java 26?</h2>
<p>You make a first HTTP/3 request by setting <code>HttpClient.Version.HTTP_3</code> on either the client builder or the request builder, then calling <code>send</code> or <code>sendAsync</code>. The rest of the API is identical to HTTP/2.</p>
<p>Here is the smallest working example you can paste into a scratch file:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Http3Hello</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpClient</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HTTP_3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">connectTimeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofSeconds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://cloudflare-quic.com/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">send</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BodyHandlers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Status: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">statusCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Negotiated: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Body bytes: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">length</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Run it on JDK 26 and you should see <code>Negotiated: HTTP_3</code>. If the server does not advertise HTTP/3 the first time you hit it, you will probably see <code>HTTP_2</code> instead. That is expected under the default discovery mode, which I will cover in the next section.</p>
<p>A few things worth calling out about this snippet.</p>
<p>First, the API surface is unchanged. The same <code>HttpRequest</code>, <code>HttpResponse</code>, and <code>BodyHandlers</code> you wrote for Java 11 keep working. Version is just a hint the client honors based on the discovery mode you pick.</p>
<p>Second, setting the version at the client level is a default, not a hard constraint. Individual requests can override it. This is useful when a single client fans out to many services and only some of them speak HTTP/3.</p>
<p>Third, <code>connectTimeout</code> still applies to the underlying QUIC handshake, so if UDP is blocked by a corporate firewall you will see a timeout instead of a hang. I lost twenty minutes to this the first time I tried it inside a locked-down VPN.</p>
<p>If you prefer async, the same request works with <code>sendAsync</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sendAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BodyHandlers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">thenApply</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">thenAccept</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>The response future resolves after the QUIC handshake and the stream completes. Combine this with virtual threads from Java 21 and you can fan out thousands of concurrent HTTP/3 requests with almost no memory overhead. More on that in the benchmark section.</p>
<h2 id="which-http3discoverymode-should-i-use">Which Http3DiscoveryMode should I use?</h2>
<p>You pick between three <code>Http3DiscoveryMode</code> values depending on how much you trust the server to actually speak HTTP/3. The choice is between safety, speed, and strictness.</p>
<p>JEP 517 introduces a new option, <code>HttpOption.H3_DISCOVERY</code>, that takes an <code>Http3DiscoveryMode</code> enum with three values: <code>ALT_SVC</code>, <code>ANY</code>, and <code>HTTP_3_URI_ONLY</code>. You set it on a request:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpOption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpOption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Http3DiscoveryMode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://example.com/api/items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setOption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpOption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">H3_DISCOVERY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Http3DiscoveryMode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ALT_SVC</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>Here is how I think about each mode.</p>
<h3 id="alt_svc-the-safe-default">ALT_SVC (the safe default)</h3>
<p><code>ALT_SVC</code> sends the first request over HTTP/2 (or HTTP/1.1), reads the <code>alt-svc</code> response header, and only switches to HTTP/3 on future requests if the server advertised it. This is the same strategy browsers have used since HTTP/3 shipped.</p>
<p>The tradeoff is clear. The first request pays the cost of a TCP and TLS handshake. Every following request to the same origin gets to use QUIC. If the server never advertises <code>alt-svc: h3=...</code>, you stay on HTTP/2 forever.</p>
<p>Use this when you are calling third-party APIs where you do not know whether HTTP/3 is supported. It is conservative and backward compatible.</p>
<h3 id="http_3_uri_only-strict-http3">HTTP_3_URI_ONLY (strict HTTP/3)</h3>
<p><code>HTTP_3_URI_ONLY</code> tries HTTP/3 first and does not fall back. If the server does not complete a QUIC handshake, the request fails with an exception.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> strict </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://quic.aiortc.org</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setOption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpOption</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">H3_DISCOVERY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Http3DiscoveryMode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HTTP_3_URI_ONLY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>Use this when you own the server and you know HTTP/3 is enabled. Or when you are running integration tests and you want to fail loudly if the path ever regresses. I like using it in dev and staging, and <code>ALT_SVC</code> in prod.</p>
<h3 id="any-parallel-race">ANY (parallel race)</h3>
<p><code>ANY</code> sends HTTP/3 and HTTP/2 in parallel and uses whichever handshake completes first. If HTTP/3 wins, you get QUIC. If the UDP path is blocked or slow, HTTP/2 takes over.</p>
<p>This is the closest you get to "just make it fast" behavior. The cost is bandwidth. You are opening two sockets for every new origin and dropping one. In a backend service talking to a known set of upstreams, that is wasteful. In a mobile or desktop client hopping across flaky networks, it is great. Pair it with a <a href="https://www.rabinarayanpatra.com/snippets/java/retry-executor-util">retry executor that whitelists transient exceptions</a> if you want a layer above the discovery mode.</p>
<h3 id="a-quick-decision-matrix">A quick decision matrix</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>First request</th>
<th>Resilience</th>
<th>Best for</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ALT_SVC</code></td>
<td>HTTP/2</td>
<td>High, falls back automatically</td>
<td>Third-party APIs, general-purpose clients</td>
</tr>
<tr>
<td><code>HTTP_3_URI_ONLY</code></td>
<td>HTTP/3</td>
<td>Low, fails if server is not on QUIC</td>
<td>Internal services you own, strict testing</td>
</tr>
<tr>
<td><code>ANY</code></td>
<td>Parallel</td>
<td>Highest</td>
<td>Unreliable networks, mobile-like clients</td>
</tr>
</tbody>
</table>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/java-26-http-3-httpclient-discovery-flow.webp" alt="HTTP/3 discovery mode decision flow from request to response version" width="1600" height="845"></p>
<p>I have not seen a case yet where I would pick <code>ANY</code> for a server-side app. But for a desktop tool running on someone else's network, it is the right default.</p>
<h2 id="what-are-the-real-benefits-of-http3-over-http2">What are the real benefits of HTTP/3 over HTTP/2?</h2>
<p>HTTP/3 wins the most on handshake latency, lossy networks, and network transitions. On a clean data center link between two Java services, the difference against HTTP/2 is small and sometimes zero.</p>
<p>Let me break that down with numbers I measured on a simple test server running caddy 2.8 with HTTP/3 enabled.</p>
<h3 id="handshake-latency">Handshake latency</h3>
<p>A fresh HTTP/2 connection needs at least two round trips: TCP three-way handshake (1 RTT) and TLS 1.3 (1 RTT). On a link with 80 ms round-trip time between my laptop and a DigitalOcean droplet in Frankfurt, that was about 165 ms before the first byte of response.</p>
<p>The same request over HTTP/3 with a cold cache was around 95 ms. QUIC folds the transport and TLS 1.3 handshake into one round trip. For a hot cache where the server certificate was already seen, the 0-RTT path cut it to roughly 50 ms.</p>
<p>That 70 to 115 ms saving per cold call matters most when your app opens a new connection per request, or when you fan out to many different origins.</p>
<h3 id="lossy-networks">Lossy networks</h3>
<p>I simulated 2% packet loss with <code>tc qdisc add dev eth0 root netem loss 2%</code> on the server. Under HTTP/2, a ten-stream fan-out stalled every time one stream hit a lost segment. The transfer that should have taken 600 ms stretched to over three seconds.</p>
<p>The same test on HTTP/3 completed in 780 ms. Only the stream that dropped a packet waited for retransmission. The others kept moving.</p>
<p>This is the reason HTTP/3 is a huge win for mobile. Cellular networks are lossy. Wi-Fi in a coffee shop is lossy. Your data center is not.</p>
<h3 id="network-transitions">Network transitions</h3>
<p>I am cheating on this one because the JDK client does not magically follow you when you switch Wi-Fi networks. But QUIC connection migration is a real thing. If the server supports it and your OS keeps the socket open, you can pop your laptop off one Wi-Fi and onto another without losing the session. The Java client exposes this through the normal socket APIs, meaning the behavior depends on your OS's UDP stack.</p>
<p>For a mobile client built on top of the Java HttpClient (think Android with JDK 26 bytecode), this is real. For a backend service running inside a single VPC, it is irrelevant.</p>
<h3 id="when-http3-does-not-help">When HTTP/3 does not help</h3>
<p>On a dedicated 10 Gbps link between two services in the same availability zone, I could not measure a meaningful difference. Packet loss was near zero, round-trip times were under 1 ms, and HTTP/2 was already using the full pipe.</p>
<p>If your service-to-service traffic lives inside a single cloud region, do not rush to HTTP/3. The wins are on the edges of your system, not the middle.</p>
<h2 id="how-does-http3-handle-streaming-downloads-in-java-26">How does HTTP/3 handle streaming downloads in Java 26?</h2>
<p>HTTP/3 in Java 26 handles streaming with the same <code>BodyHandlers</code> you used for HTTP/2. <code>ofInputStream</code>, <code>ofLines</code>, <code>ofByteArrayConsumer</code>, and the reactive <code>ofPublisher</code> all work unchanged.</p>
<p>Here is a streaming download that writes directly to disk without buffering the whole payload:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">nio</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">file</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Http3Download</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpClient</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HTTP_3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://example.com/big.bin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">send</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BodyHandlers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofFile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">big.bin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Saved to: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>QUIC has a nice property when streaming big payloads: because it is a datagram transport with its own flow control, it does not suffer from TCP's bufferbloat problem in the same way. I tested this with a 1 GB file over a 50 Mbps link with 120 ms RTT. HTTP/2 averaged around 44 Mbps with visible stalls. HTTP/3 averaged 48 Mbps with smoother throughput.</p>
<p>For very large downloads where you want progress reporting, pair <code>ofInputStream</code> with a counting wrapper:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">InputStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">send</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BodyHandlers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofInputStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">InputStream</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> in </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">     OutputStream</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> out </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Files</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newOutputStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">big.bin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    byte</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> buf </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> byte</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">64</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1024</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">];</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> total </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> read</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    while</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ((</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">read </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> in</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">read</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">buf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">write</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">buf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> read</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        total </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> read</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">total </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">%</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x3C;&#x3C;</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 20</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">printf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Downloaded %d MB%n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> total </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">>></span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 20</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That code is version-agnostic. Swap <code>HTTP_3</code> for <code>HTTP_2</code> in the client builder and it still works. That is the core promise of JEP 517: one API, many protocols.</p>
<h2 id="how-do-i-benchmark-http3-vs-http2-in-java-26">How do I benchmark HTTP/3 vs HTTP/2 in Java 26?</h2>
<p>You benchmark HTTP/3 against HTTP/2 in Java 26 by building two clients with different versions, running the same workload through each, and measuring both wall-clock time and percentile latencies. Keep the rest of the code identical.</p>
<p>I use this shape of benchmark. It is not JMH-grade, but it is good enough to see real differences.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">net</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">util</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">util</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">concurrent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> java</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">util</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">IntStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Http3Benchmark</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TOTAL_REQUESTS </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> URI</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TARGET </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> URI</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://myserver.example.com/ping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        runWith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HTTP_2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">HTTP/2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        runWith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HTTP_3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">HTTP/3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> runWith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Version</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> label</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpClient</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">connectTimeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofSeconds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">10</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TARGET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Instant</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> start </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Void</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> futures </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> IntStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TOTAL_REQUESTS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">mapToObj</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sendAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> HttpResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BodyHandlers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">discarding</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">futures</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toArray</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">new</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">])).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ms </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">between</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> successes </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> futures</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">filter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">r </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">statusCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 200</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">count</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">printf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">%s: %d/%d in %d ms (%.2f req/s)%n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            label</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> successes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TOTAL_REQUESTS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">successes </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1000.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> /</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Two warnings about running this.</p>
<p>First, warm up the JVM before you trust the numbers. Either call <code>runWith</code> for both versions twice and throw away the first run, or wrap it in JMH. I have been burned too many times by benchmarking a cold JIT.</p>
<p>Second, the latency difference between HTTP/2 and HTTP/3 on a LAN is often smaller than the variance between runs. Run each test at least five times and report the median. Do not trust a single run, and definitely do not trust me.</p>
<p>On my Frankfurt droplet with 80 ms RTT, 1000 concurrent GETs against a <code>/ping</code> endpoint that returned 128 bytes gave me:</p>
<ul>
<li>HTTP/2: around 3.4 seconds (295 req/s)</li>
<li>HTTP/3 with <code>ALT_SVC</code>: around 3.1 seconds (322 req/s)</li>
<li>HTTP/3 with <code>HTTP_3_URI_ONLY</code>: around 2.6 seconds (385 req/s)</li>
</ul>
<p>That last number is the one people quote when they say HTTP/3 is faster. It is real, but it only shows up when the first request is already on HTTP/3. <code>ALT_SVC</code> costs you that first HTTP/2 hit.</p>
<p>Pair this with <a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">virtual threads from Java 25</a> for even better fan-out. A virtual thread per request plus <code>sendAsync</code> gives you thousands of concurrent requests with no thread pool tuning.</p>
<h2 id="what-are-the-common-pitfalls-when-enabling-http3-in-java">What are the common pitfalls when enabling HTTP/3 in Java?</h2>
<p>The top pitfalls are UDP being blocked by firewalls, the JDK not shipping with an HTTP/3-capable default trust store setting, and assuming HTTP/3 always wins on latency. All three have bitten me.</p>
<h3 id="udp-drops-in-corporate-networks">UDP drops in corporate networks</h3>
<p>HTTP/3 runs on UDP port 443. A lot of corporate firewalls, VPN clients, and older NAT devices drop or rate-limit UDP on that port because it looks a lot like QUIC probing or DNS abuse.</p>
<p>The symptom is a timeout on the first request when you use <code>HTTP_3_URI_ONLY</code>, or silent fallback to HTTP/2 when you use <code>ALT_SVC</code>. The fix is not code: it is a network ticket. But until that ticket is resolved, stay on <code>ALT_SVC</code>.</p>
<p>You can check quickly with:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">nc</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -u</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -zv</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> cloudflare-quic.com</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 443</span></span></code></pre></figure>
<p>If that hangs, HTTP/3 is not reaching Cloudflare from your box.</p>
<h3 id="trust-store-and-certificate-quirks">Trust store and certificate quirks</h3>
<p>QUIC requires TLS 1.3. If you are still using a custom <code>SSLContext</code> that forces TLS 1.2, you will not complete an HTTP/3 handshake.</p>
<p>The fix is to let the default <code>SSLContext</code> handle negotiation. If you absolutely need a custom context, make sure it includes TLS 1.3 and does not disable the cipher suites QUIC requires. On the JDK 26 default build, TLS 1.3 with the standard modern suites is enabled.</p>
<h3 id="connection-pooling-behavior">Connection pooling behavior</h3>
<p><code>HttpClient</code> reuses connections across requests to the same origin. For HTTP/3, the unit of reuse is a QUIC connection, identified by the connection ID. If you are creating a new <code>HttpClient</code> per request in your code (please do not), every request pays a full QUIC handshake.</p>
<p>The fix is the same as for HTTP/2: create the client once, hold onto it, and let it pool. The convention is a single client per application module.</p>
<h3 id="assuming-http3-is-always-faster">Assuming HTTP/3 is always faster</h3>
<p>It is not. On clean networks, HTTP/2 is competitive or even faster because its congestion control is more mature and the TCP stack in the kernel has decades of tuning behind it.</p>
<p>QUIC's user-space implementation is catching up, but benchmarks still show HTTP/2 winning on short, clean links. Measure in your environment before switching. The default in Java 26 is still HTTP/2 for a reason.</p>
<h2 id="when-should-i-actually-opt-in-to-http3-in-java-26">When should I actually opt in to HTTP/3 in Java 26?</h2>
<p>You should opt in to HTTP/3 when your Java client talks to mobile, public, or lossy networks, or when the upstream is a CDN that already speaks QUIC at scale. Stick to HTTP/2 for internal service-to-service calls inside a single cloud region.</p>
<p>Here is the checklist I apply before flipping the switch on a real project:</p>
<ol>
<li>Does my Java app talk to endpoints behind Cloudflare, Fastly, Akamai, or AWS CloudFront? All four have first-class HTTP/3 support. Flip the switch.</li>
<li>Is my app a desktop tool, a scraper, or a CLI that runs on random networks? Use <code>ALT_SVC</code> as the default. The 70 ms handshake saving on repeat requests adds up.</li>
<li>Do I control both client and server, and do they live in the same VPC? Leave it on HTTP/2. The wins are not worth the operational cost of a new transport.</li>
<li>Am I writing a mobile client on Android 15+ that ships with JDK 26 bytecode? Absolutely opt in. Network transitions between Wi-Fi and cellular are the single biggest practical win.</li>
<li>Am I hitting a server behind a corporate firewall? Check UDP port 443 first. If it is blocked, HTTP/3 is a nonstarter until the network team unblocks it.</li>
</ol>
<p>The real meta-answer is that HTTP/3 is a protocol feature, not a silver bullet. Java 26 finally put it in the standard library. That is the feature. Now your codebase decides when to use it.</p>
<p>I have been running <code>ALT_SVC</code> in a scraper that hits a thousand different origins per minute for two weeks. Roughly 38% of those origins advertised HTTP/3. The fall-through cost is near zero, and the upgrade on the ones that speak QUIC is basically free. Good tradeoff.</p>
<p>For an internal REST API between two Spring Boot services? I am staying on HTTP/2. Not worth the on-call risk yet.</p>
<h2 id="what-is-next-for-java-networking">What is next for Java networking?</h2>
<p>HTTP/3 is the headline change, but JDK 26 also brought a new cryptography API and more HTTP client quality-of-life improvements worth tracking. Server-side QUIC is the obvious next step, and it is not in the JDK yet. For now, if you want a Java HTTP/3 server, you are back to Netty or Helidon.</p>
<p>The broader point is that the JDK is catching up to where the web has already gone. HTTP/3, virtual threads, structured concurrency, ZGC generational improvements. The standard library is finally a credible default again for high-scale network code, instead of a starting point you immediately replace.</p>
<p>For more on JEP 517 and Java 26, see the <a href="https://openjdk.org/projects/jdk/26/">official JDK 26 project page</a>, the <a href="https://inside.java/2026/03/04/jdk-26-http-client/">Inside.java HTTP Client deep dive</a>, and the <a href="https://www.oracle.com/java/technologies/javase/26all-relnotes.html">consolidated JDK 26 release notes</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Virtual Threads in Java 25: The Complete Guide</a> — Pair HTTP/3 with virtual threads for millions of concurrent requests with no pool tuning.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-25-compact-object-headers">Java 25 Compact Object Headers</a> — Another JVM-level change that pays off most at scale, similar in spirit to HTTP/3.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java in Spring Boot</a> — How the latest JDK features land in the Spring ecosystem, including networking stacks.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Claude Opus 4.7: Pricing, Benchmarks & Breaking Changes]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide</guid>
      <pubDate>Thu, 16 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Claude Opus 4.7 API pricing per million tokens, SWE-bench benchmarks, new tokenizer, and the four API breaking changes since Opus 4.6. Full migration guide.]]></description>
      <content:encoded><![CDATA[<p>Anthropic shipped Claude Opus 4.7 yesterday, and if you are on Opus 4.6 in production, your next deploy is going to break. I spent yesterday evening migrating my own Claude Code setup, my API agent loops, and a small app I maintain that calls Claude directly. The new model is faster at real coding work and it sees screenshots at a resolution I did not think we would hit this year. It is also the first Claude release where the upgrade needed actual code changes on my side.</p>
<p>This is the full deep dive and migration guide I wish someone had handed me yesterday. Benchmarks, every new feature, every breaking change, a real cost estimate for the new tokenizer, and a migration checklist you can run through in one sitting. By the end you will know whether to upgrade today, next week, or wait.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-opus-4-7-release-and-migration-guide.webp" alt="Claude Opus 4.7 release hero image with benchmark scores" width="1600" height="845"></p>
<h2 id="what-is-claude-opus-47-and-why-does-it-matter">What is Claude Opus 4.7 and why does it matter?</h2>
<p>Claude Opus 4.7 is Anthropic's most capable generally available model, released on April 16, 2026 across Claude products, the API, Amazon Bedrock, Google Cloud Vertex AI, and Microsoft Foundry. The API model ID is <code>claude-opus-4-7</code>. Pricing sits at $5 per million input tokens and $25 per million output tokens, the same sticker price as Opus 4.6.</p>
<p>The context window is 1M tokens at standard pricing with no long-context premium, and the max output is 128k tokens. If you ever priced out a 1M-token request on another vendor, you already know how unusual that is.</p>
<p>There is a second thing worth stating out loud. Opus 4.7 sits between the Opus 4.6 release from February and the still-gated Claude Mythos that shipped on April 7 through Project Glasswing. Mythos is the one Anthropic refuses to put on the public API because it can autonomously find real zero-days. Opus 4.7 is the model most of us actually get to run, and it is the one reclaiming SOTA on coding benchmarks from GPT-5.4 this week.</p>
<p>In my head, the quick mental model is: Opus 4.6 is the last of the old regime, Mythos is the one you cannot have, and Opus 4.7 is the production daily driver for the rest of the quarter.</p>
<h2 id="how-much-better-is-opus-47-at-coding-benchmarks">How much better is Opus 4.7 at coding benchmarks?</h2>
<p>Opus 4.7 is meaningfully better than Opus 4.6 on every coding benchmark Anthropic publishes, and it beats GPT-5.4 on SWE-bench Pro by about 7 points. The numbers are not marketing vibes. They are the same benchmarks every vendor publishes, and this is the first time in months a public Claude release has taken the top slot on agentic coding.</p>
<p>Here is the short version of the scorecard.</p>
<table>
<thead>
<tr>
<th>Benchmark</th>
<th>Opus 4.6</th>
<th>Opus 4.7</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>SWE-bench Verified</td>
<td>80.8%</td>
<td><strong>87.6%</strong></td>
<td>Nearly 7-point jump</td>
</tr>
<tr>
<td>SWE-bench Pro</td>
<td>53.4%</td>
<td><strong>64.3%</strong></td>
<td>Beats GPT-5.4 at 57.7%</td>
</tr>
<tr>
<td>Terminal-Bench 2.0</td>
<td>-</td>
<td><strong>69.4%</strong></td>
<td>New high for Claude</td>
</tr>
<tr>
<td>Rakuten-SWE-Bench</td>
<td>baseline</td>
<td><strong>3x</strong> production tasks resolved</td>
<td>Real enterprise tasks</td>
</tr>
<tr>
<td>Internal coding eval</td>
<td>baseline</td>
<td><strong>+13%</strong> resolution</td>
<td>Anthropic internal</td>
</tr>
</tbody>
</table>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-opus-4-7-release-and-migration-guide/benchmarks-chart.webp" alt="Claude Opus 4.7 vs 4.6 vs GPT-5.4 coding benchmark comparison chart" width="1600" height="845"></p>
<p>Two things stand out beyond the raw numbers. First, Anthropic says Opus 4.7 shows a 14% improvement on complex multi-step workflows while using fewer tokens and producing roughly one third the tool errors of 4.6. Fewer tool errors is the quieter story here. If you have ever watched an agent loop rack up retries because a tool call went sideways, one third the errors compounds into real latency and real cost savings.</p>
<p>Second, Opus 4.7 is the first Claude model to pass what Anthropic calls implicit-need tests. Those are tasks where the model has to infer which tool or action is required, instead of being told directly in the prompt. In my own Claude Code sessions yesterday, this was the single biggest felt difference. I used to prefix prompts with "check git log before editing" out of habit. With 4.7, I stopped doing that and it checked anyway.</p>
<p>You can verify the numbers for yourself at the <a href="https://www.anthropic.com/news/claude-opus-4-7">official Claude Opus 4.7 announcement</a> and the <a href="https://venturebeat.com/technology/anthropic-releases-claude-opus-4-7-narrowly-retaking-lead-for-most-powerful-generally-available-llm">VentureBeat launch writeup</a>.</p>
<h2 id="what-new-features-does-opus-47-add">What new features does Opus 4.7 add?</h2>
<p>Opus 4.7 adds three developer-facing features that actually change how you write agent code. Those are the <code>xhigh</code> effort level, task budgets, and high-resolution vision at 3.75 megapixels.</p>
<h3 id="the-new-xhigh-effort-level">The new xhigh effort level</h3>
<p>The <code>effort</code> parameter already lets you trade intelligence for speed and cost, but 4.7 introduces a new level called <code>xhigh</code> that slots between <code>high</code> and <code>max</code>. Anthropic recommends starting at <code>xhigh</code> for coding and agentic tasks, and at least <code>high</code> for anything else that is intelligence-sensitive.</p>
<p>Claude Code defaults to <code>xhigh</code> across every plan now. That is a big deal because it means the same prompt that cost you X tokens yesterday will silently cost more today unless you explicitly lower the effort.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Claude Opus 4.7 with explicit effort</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-4-7</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">64000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    output_config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">effort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">xhigh</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[{</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Refactor this service layer.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<h3 id="task-budgets-the-advisory-cap-for-agentic-loops">Task budgets, the advisory cap for agentic loops</h3>
<p>Task budgets are the new feature I am most excited about. A task budget is a rough token target for the full agentic loop, including thinking, tool calls, tool results, and final output. The model sees a running countdown and uses it to prioritize work and finish the task before it runs out.</p>
<p>The important distinction is that <code>max_tokens</code> is a hard per-request cap that the model never sees. A task budget is advisory, the model is aware of it, and it self-moderates. You set both at once for agent work that has a real budget ceiling but needs the model to plan.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">beta</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-4-7</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    max_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">128000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    output_config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">effort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">high</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">task_budget</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">total</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">role</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Review the codebase and propose a refactor plan.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    ],</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    betas</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">task-budgets-2026-03-13</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>A few rules to remember. The minimum budget is 20k tokens. Budgets are not hard caps, so the model can go over if it really needs to. Do not set a budget for open-ended tasks where quality matters more than speed, because a tight budget makes the model cut corners. The full spec is on the <a href="https://platform.claude.com/docs/en/build-with-claude/task-budgets">task budgets documentation page</a>.</p>
<h3 id="high-resolution-vision-at-375-megapixels">High-resolution vision at 3.75 megapixels</h3>
<p>Opus 4.7 is the first Claude model that accepts images up to 2576 pixels on the long edge, or 3.75 megapixels. That is more than three times the resolution ceiling of 1568 pixels on earlier Claude models. Coordinates are now 1:1 with actual pixels, so there is no scale-factor math when the model returns bounding boxes.</p>
<p>This matters most for computer use, screenshot agents, and document understanding. I had an internal screenshot workflow that was choking on a dense admin dashboard because small buttons blurred out at 1.15 megapixels. Re-running it on 4.7 at full resolution, the model found the buttons on the first try. One caveat: high-resolution images use more tokens, so downsample if you do not need the fidelity. Vision details and examples are on the <a href="https://platform.claude.com/docs/en/build-with-claude/vision">Images and vision docs page</a>.</p>
<h2 id="what-breaking-changes-should-you-know-before-upgrading-from-opus-46">What breaking changes should you know before upgrading from Opus 4.6?</h2>
<p>The Messages API has four breaking changes on Claude Opus 4.7 that will make existing code throw 400s or silently behave differently. Claude Managed Agents is unaffected, so if you only use Managed Agents you can skip this section.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-opus-4-7-release-and-migration-guide/effort-ladder.webp" alt="Claude Opus 4.7 effort ladder and task budget flow diagram" width="1600" height="845"></p>
<h3 id="extended-thinking-budgets-are-removed">Extended thinking budgets are removed</h3>
<p>The <code>thinking.budget_tokens</code> field is gone. Passing <code>thinking: {"type": "enabled", "budget_tokens": N}</code> now returns a 400 error. Adaptive thinking is the only thinking-on mode, and Anthropic says it reliably outperforms budgeted extended thinking on internal evals.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Before (Opus 4.6)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">thinking </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">budget_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 32000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># After (Opus 4.7)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">thinking </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">adaptive</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">output_config </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">effort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">high</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="adaptive-thinking-is-off-by-default">Adaptive thinking is off by default</h3>
<p>This is the silent one. Requests with no <code>thinking</code> field now run without thinking at all. On Opus 4.6 you had thinking by default in most setups. You now have to set <code>thinking: {"type": "adaptive"}</code> explicitly if you want any reasoning on the request.</p>
<p>If you forget this, your output quality on hard tasks will drop and you will not see a 400. Just worse answers. Watch out.</p>
<h3 id="sampling-parameters-are-removed">Sampling parameters are removed</h3>
<p>Setting <code>temperature</code>, <code>top_p</code>, or <code>top_k</code> to any non-default value returns a 400 on Opus 4.7. This one tripped me up on a tool that was hardcoded to <code>temperature=0</code> for "determinism."</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># This will now 400 on claude-opus-4-7</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">messages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    model</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">claude-opus-4-7</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">    temperature</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0.7</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  # BadRequestError: sampling parameters not supported</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">    ...</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>The safest migration is to drop these fields from every request and guide behavior via prompting instead. If you were using <code>temperature=0</code> for determinism, note that it never actually guaranteed identical outputs anyway. The expectation was always probabilistic.</p>
<h3 id="thinking-content-is-omitted-by-default">Thinking content is omitted by default</h3>
<p>Thinking blocks still show up in the response stream, but their <code>thinking</code> field is empty unless you opt back in. The reasoning still happens. You just do not see it.</p>
<p>If your UI streams reasoning to users as it arrives, the new default looks like a long blank pause before the final output starts. Users will notice. The fix is one line:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="python" data-theme="material-theme github-light"><code data-language="python" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">thinking </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">    "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">adaptive</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">    "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">display</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">summarized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  # "omitted" is the new default</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The full list of breaking changes is on the <a href="https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7">What's new in Claude Opus 4.7 docs page</a>.</p>
<h2 id="how-does-the-new-tokenizer-change-your-real-costs">How does the new tokenizer change your real costs?</h2>
<p>Opus 4.7 ships a new tokenizer that can use 1x to 1.35x the tokens of Opus 4.6 for the same input text. Pricing is identical per token, but that 35% ceiling means real invoices go up for most workloads even when nothing else changes.</p>
<p>I ran a quick test yesterday on an agent loop that summarizes GitHub PRs. Same prompt, same PR, same effort level. Here is what I measured on my setup.</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Opus 4.6</th>
<th>Opus 4.7</th>
<th>Delta</th>
</tr>
</thead>
<tbody>
<tr>
<td>Input tokens</td>
<td>12,430</td>
<td>14,820</td>
<td>+19%</td>
</tr>
<tr>
<td>Output tokens</td>
<td>2,110</td>
<td>2,480</td>
<td>+17%</td>
</tr>
<tr>
<td>Total cost</td>
<td>$0.115</td>
<td>$0.136</td>
<td>+18%</td>
</tr>
</tbody>
</table>
<p>This is one data point on one workload, so take it as directional rather than authoritative. On plain English the tokenizer seems close to parity. On code-heavy prompts the gap widens. Anthropic's own docs confirm the 1.0x to 1.35x range varies by content type.</p>
<p>There are three mitigations that actually work.</p>
<ol>
<li><strong>Set a task budget.</strong> The model sees the countdown and prioritizes. On my PR summarizer, a 10k task budget pulled output costs back to near parity with 4.6.</li>
<li><strong>Drop from xhigh back to high.</strong> The jump from high to xhigh is large. If your workload is not reasoning-bound, high is usually fine and saves you the biggest chunk.</li>
<li><strong>Rebudget your <code>max_tokens</code>.</strong> If you set <code>max_tokens</code> based on 4.6 token counts, add headroom. This also matters for compaction triggers in agent loops that summarize when approaching the ceiling.</li>
</ol>
<p>If you want the full cost write-up with more workload types, <a href="https://www.finout.io/blog/claude-opus-4.7-pricing-the-real-cost-story-behind-the-unchanged-price-tag">Finout published a good breakdown on the real cost story behind the unchanged price tag</a>.</p>
<h2 id="how-do-you-migrate-an-existing-opus-46-app-to-47">How do you migrate an existing Opus 4.6 app to 4.7?</h2>
<p>The migration from Opus 4.6 to 4.7 is a one-sitting job for most apps. Here is the checklist I used on my own code yesterday. It works whether you are on the raw SDK, the <a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">AI SDK through Vercel AI Gateway</a>, or LangChain.</p>
<h3 id="step-1-update-the-model-id">Step 1: Update the model ID</h3>
<p>Change every <code>claude-opus-4-6</code> string to <code>claude-opus-4-7</code>. Search and replace. Check config files, prompts stored in databases, and feature flags that select the model at runtime.</p>
<h3 id="step-2-remove-sampling-parameters">Step 2: Remove sampling parameters</h3>
<p>Grep for <code>temperature</code>, <code>top_p</code>, and <code>top_k</code> across your codebase.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -rE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">(temperature|top_p|top_k)\s*[:=]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> src/</span></span></code></pre></figure>
<p>Delete every occurrence for Claude calls. If your code shares the same request builder across providers, gate the Claude branch separately or remove the fields for all models.</p>
<h3 id="step-3-swap-extended-thinking-for-adaptive-thinking">Step 3: Swap extended thinking for adaptive thinking</h3>
<p>Search for <code>budget_tokens</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -rn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">budget_tokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> src/</span></span></code></pre></figure>
<p>Replace with adaptive thinking plus an explicit effort level. If you used extended thinking budgets to control cost, task budgets are the replacement.</p>
<h3 id="step-4-decide-on-thinking-visibility">Step 4: Decide on thinking visibility</h3>
<p>If your UI streams reasoning to users, add <code>"display": "summarized"</code> so they see progress. If you only log thinking server-side for analysis, leave the default.</p>
<h3 id="step-5-rebudget-max_tokens-for-the-new-tokenizer">Step 5: Rebudget <code>max_tokens</code> for the new tokenizer</h3>
<p>Bump <code>max_tokens</code> on requests where you were running close to the ceiling. A 20% buffer covers most cases. Also check any prompt-compaction thresholds.</p>
<h3 id="step-6-re-test-prompts-that-relied-on-46s-warmer-tone">Step 6: Re-test prompts that relied on 4.6's warmer tone</h3>
<p>Opus 4.7 is more direct and more literal. If you had prompts that said "be friendly and use emoji," those still work. If you had prompts that relied on the model silently generalizing an instruction from one item to another, 4.7 will not do that. Strip the scaffolding that told 4.6 to "double-check the slide layout" or "verify the output before returning," because 4.7 does that on its own now.</p>
<h3 id="step-7-check-for-cyber-safeguards">Step 7: Check for cyber safeguards</h3>
<p>Opus 4.7 ships with real-time cybersecurity safeguards that refuse certain high-risk requests even for legitimate security research. If your product does security work, apply to Anthropic's <a href="https://claude.com/form/cyber-use-case">Cyber Verification Program</a> to preserve access.</p>
<p>The full step-by-step including automated codemods is on the <a href="https://platform.claude.com/docs/en/about-claude/models/migration-guide">Claude API migration guide</a>.</p>
<h2 id="should-you-upgrade-to-opus-47-now">Should you upgrade to Opus 4.7 now?</h2>
<p>For most builders, upgrade this week. The coding benchmark gains are real, the agentic improvements show up in fewer tool retries and smarter tool selection, and the cost ceiling is manageable once you know about the tokenizer bump.</p>
<p>Here is my honest call by workload type.</p>
<ul>
<li><strong>Agentic coding, Claude Code users, tool-heavy agents:</strong> upgrade today. The implicit-need inference alone changes the prompt-engineering tax on every session.</li>
<li><strong>Computer use, screenshot agents, document analysis:</strong> upgrade today. The 3.75 megapixel jump is the biggest vision upgrade in a Claude release since vision shipped.</li>
<li><strong>Plain chat apps with no tools:</strong> no rush. You get a small quality bump and a small cost bump. Pick a low-traffic Tuesday.</li>
<li><strong>Apps that hardcode <code>temperature=0</code> for "determinism":</strong> do the migration work first, then ship. You will 400 on every request until the sampling fields come out.</li>
<li><strong>Products that stream thinking to end users:</strong> migrate with the one-line <code>display</code> fix in the same deploy. Otherwise users see a dead pause before output and file bug reports.</li>
<li><strong>Security research tooling:</strong> apply to the Cyber Verification Program before the upgrade, not after. The new safeguards are stricter than 4.6's.</li>
</ul>
<p>Also apply common sense. Run the upgrade on a preview environment before prod. Watch your cost dashboard for a week. If your task success rates hold and your invoice is within 20% of what you budgeted, you are in good shape.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Claude Opus 4.7 is the upgrade I have been waiting for, but it is also the first Claude release that required me to actually read the docs. The coding jump is real, the agent loop is smoother, and the vision resolution finally matches what Retina screenshots actually capture. At the same time, four breaking Messages API changes and a new tokenizer mean you cannot just bump the model ID and redeploy.</p>
<p>If you take one thing from this post, make it the migration checklist. Work through steps 1 to 7 on a branch today, run your tests, watch the cost dashboard for a day, and ship. That is the whole job. My prediction for the quarter is that we are going to see the agent tooling ecosystem reorganize around task budgets as the new primitive, because a hard advisory cap that the model self-moderates against is a much cleaner abstraction than stitching manual token counting into every custom loop. Watch for that pattern to show up in every major framework within a few weeks.</p>
<p>For more on Claude Opus 4.7, see the <a href="https://www.anthropic.com/news/claude-opus-4-7">official Anthropic announcement</a>, the <a href="https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7">What's new in Claude Opus 4.7 docs</a>, and the <a href="https://thenextweb.com/news/anthropic-claude-opus-4-7-coding-agentic-benchmarks-release">detailed benchmark reporting from The Next Web</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-mythos-project-glasswing">Claude Mythos and Project Glasswing</a>: the gated model that sits above Opus 4.7, why you cannot have it, and what that signals about where Anthropic is headed.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects">Claude Skills vs MCP vs Projects</a>: how the three Claude extensibility systems fit together, and where Opus 4.7's better memory tool lands in that picture.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/vercel-ai-gateway-deep-dive">Vercel AI Gateway Deep Dive</a>: route Opus 4.7 through the AI Gateway for cost tracking, fallbacks, and per-model observability without rewriting your client code.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/google-antigravity">Google Antigravity</a>: the competing agentic coding platform, for context on where Cursor, Windsurf, and Anthropic's own Claude Code fit in the same week.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Claude Skills vs MCP vs Projects: Which One Should You Use?]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects</link>
      <guid>https://www.rabinarayanpatra.com/blogs/claude-skills-vs-mcp-vs-projects</guid>
      <pubDate>Wed, 15 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Claude Skills, MCP servers, and Projects solve different problems. Here is the honest comparison, token costs, and a decision framework for picking right.]]></description>
      <content:encoded><![CDATA[<p>I spent a weekend rebuilding my blog's assistant setup three times. First with Claude Projects, then with an MCP server, then with Skills. Each time I thought I had the right answer. Each time I was wrong, but for a different reason.</p>
<p>The fourth attempt got me where I wanted to go, and the fix was embarrassing. I did not need to pick one. I needed all three, doing different jobs. This post is the guide I wish I had read before burning 20 hours figuring that out. If you are trying to decide between Claude Skills, MCP servers, and Projects, here is the honest comparison with token costs, pricing, and a decision framework that actually works.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-skills-vs-mcp-vs-projects/skills-context-window.webp" alt="Claude Skills context window loading diagram from the official Anthropic documentation" width="1200" height="676">
<em>Source: <a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview">Anthropic Agent Skills documentation</a></em></p>
<h2 id="what-actually-is-a-claude-skill-an-mcp-server-and-a-project">What actually is a Claude Skill, an MCP server, and a Project?</h2>
<p>A Claude Skill is a folder of instructions that Claude loads on demand, an MCP server is a program that exposes tools and data to Claude over a standard protocol, and a Project is a persistent workspace with a knowledge base and custom instructions scoped to one body of work. Same AI model, three different ways to extend what it knows and what it can do.</p>
<p>The cleanest framing I have seen comes from Anthropic's own team. They describe it like this. Projects say "here is what you need to know." Skills say "here is how to do things." MCP says "here is a tool you can use." If you want an even simpler analogy, MCP hands Claude a hammer, Skills teach it how to drive nails, and Projects are the workshop where the blueprints live.</p>
<p>Here is what each one actually looks like on disk or in the API.</p>
<p>A Skill is a folder with a <code>SKILL.md</code> file at the root. The top of that file is YAML frontmatter with two required fields, <code>name</code> and <code>description</code>. Everything below the frontmatter is markdown instructions. You can include helper files and executable scripts alongside <code>SKILL.md</code>, and Claude will read or run them only when the task needs them.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">---</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pdf-processing</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">---</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># PDF Processing</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">## Quick start</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Use pdfplumber to extract text from PDFs.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">For advanced form filling, see [FORMS.md](FORMS.md).</span></span></code></pre></figure>
<p>An MCP server is a separate process. It speaks JSON-RPC 2.0 over either stdin/stdout for local use or Streamable HTTP for remote use. Claude Code or Claude Desktop spawns an MCP client that connects to the server, negotiates capabilities, and then calls the tools the server exposes.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-skills-vs-mcp-vs-projects/mcp-architecture.webp" alt="MCP client-server architecture showing an MCP Host with three MCP Clients connecting to local filesystem and database servers via stdio, and a remote Sentry server via Streamable HTTP" width="1200" height="633">
<em>Architecture based on the <a href="https://modelcontextprotocol.io/docs/learn/architecture">Model Context Protocol architecture overview</a></em></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">jsonrpc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2.0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">tools/call</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">params</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">weather_current</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">arguments</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">location</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">San Francisco</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      "</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">units</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">imperial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>A Project is a workspace inside claude.ai. You click New Project, give it a name, paste in custom instructions, and upload files to its knowledge base. Everything you upload becomes available in every chat inside that project. No YAML, no server, just a scoped conversation bucket with a big context window attached to it.</p>
<p>Those three shapes matter because they drive every tradeoff that follows. A folder of markdown is cheap to write and cheap to load. A separate process is powerful but expensive in both setup and context. A workspace is great for scoping but has no way to do things, only to remember things.</p>
<h2 id="how-does-each-one-load-into-claudes-context">How does each one load into Claude's context?</h2>
<p>Skills load in three stages using progressive disclosure, MCP loads its entire tool schema upfront on every request, and Projects load their knowledge base statically whenever you are inside the project. This is the single most important difference between them, and nobody explains it clearly.</p>
<p>Let me break down exactly what enters Claude's context window in each case.</p>
<p><strong>Skills use progressive disclosure</strong>. At startup, Claude only sees the <code>name</code> and <code>description</code> from the YAML frontmatter. That costs about 100 tokens per skill. You can install dozens of skills and Claude still only pays that flat 100-token entry fee for each one. When your request matches a skill's description, Claude reads the full <code>SKILL.md</code> body via bash. That adds under 5,000 tokens. If <code>SKILL.md</code> references another file, say <code>FORMS.md</code> or <code>scripts/validate.py</code>, Claude reads or runs that file only when the task actually needs it. Scripts are especially efficient. When Claude runs a script, the code itself never enters context. Only the script's output does.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-skills-vs-mcp-vs-projects/skills-architecture.webp" alt="Agent Skills architecture diagram showing how Skills integrate with Claude&#x27;s virtual machine" width="1200" height="675">
<em>Source: <a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview">Anthropic Agent Skills documentation</a></em></p>
<p><strong>MCP does the opposite</strong>. When Claude connects to an MCP server, it calls <code>tools/list</code> and loads the full JSON schema of every tool the server exposes. That schema stays in context for the whole session. GitHub's official MCP server, which exposes roughly 80 tools, costs tens of thousands of tokens just to load the tool definitions. You pay that upfront, every single conversation, whether you use GitHub or not.</p>
<p><strong>Projects load their knowledge base statically</strong>. When you open a chat inside a project, the custom instructions and uploaded files are always in context. If your project knowledge base is small, it all fits. If it is large, Claude quietly switches to RAG mode and retrieves the relevant chunks instead, which can expand usable capacity by up to ten times but trades determinism for retrieval accuracy.</p>
<p>The practical consequence is simple. Skills scale to many capabilities without bloating context. MCP gives you raw power at a real token cost. Projects are static, so they work well when the knowledge base matches the work but feel wasteful when you keep opening a big project to ask small questions.</p>
<h2 id="when-should-you-reach-for-a-skill-instead-of-a-project">When should you reach for a Skill instead of a Project?</h2>
<p>Reach for a Skill when you are encoding a repeatable procedure, and use a Project when you are scoping a body of knowledge. The split is procedure versus context. Skills travel with you. Projects pin context to one workspace.</p>
<p>Here is the test I use. If I find myself copying the same instruction block into three different chats, that instruction should be a Skill. If I keep uploading the same PDF to three different chats, that PDF should be in a Project. If I keep doing both, the Project holds the PDF and the Skill handles the instructions.</p>
<p>A concrete example from my own setup. I write blog posts on rabinarayanpatra.com, and I have strict standards for how every post should be structured. H2 headings must be questions. First sentence after each H2 must answer it. No em dashes. No AI-tell words from my banned list. I used to paste those rules into every chat. That was fine until I started writing posts across three different projects. Then I had the same rules pasted three times, drifting slightly each time.</p>
<p>I moved the rules into a Skill called <code>blog-standards</code>. Now any chat, in any project or none, can load those rules on demand. The project for my portfolio still exists, but it holds my actual post drafts and research, not the rules for writing them.</p>
<p>That split feels obvious in hindsight. Before I made it, I was using Projects as a dumping ground for both knowledge and procedure, and it was bloating every conversation.</p>
<p>Another clean test is portability. Skills work everywhere. Create a skill once, and it is available in regular chats, inside any project, across all surfaces where Skills are supported. Projects are scoped by definition. You cannot take a project with you into a regular chat. If the thing you are teaching Claude needs to apply in many contexts, it is a Skill.</p>
<h2 id="when-does-mcp-beat-a-skill-and-when-does-it-lose">When does MCP beat a Skill, and when does it lose?</h2>
<p>MCP beats a Skill when Claude needs to reach external systems that Skills cannot touch, and MCP loses when the task is really about procedural knowledge dressed up as tool calls. The line is reachability. MCP can talk to your database, your Jira, your production API, your Slack. Skills cannot. Skills run in a sandbox without network access on the API, or with user-machine access in Claude Code.</p>
<p>Start with the cases where MCP is the right answer.</p>
<p>Claude needs to read data that lives somewhere else. A live database, a SaaS with an API, your company's Notion or Confluence. You want Claude to write to those same systems, like creating a Jira ticket or posting to Slack. You want the same integration to work across Claude Desktop, Claude Code, Cursor, VS Code, and any other MCP client. That last point matters more than people realize. MCP is an open standard under the Linux Foundation, and it has crossed 10,000 active servers with 97 million monthly SDK downloads. If you are going to invest in an integration, MCP is the one that outlives any single vendor's product decisions.</p>
<p>Now the other side.</p>
<p>I keep seeing teams reach for MCP when a Skill would have been faster, cheaper, and more reliable. The symptom is always the same. Someone builds an MCP server that is really a wrapper around a CLI or a local script, loads it into every session, pays the tool schema cost every conversation, and then uses it maybe once a week. Simon Willison nailed it when he wrote that "almost everything I might achieve with an MCP can be handled by a CLI tool instead." A Skill that wraps that same CLI costs 100 tokens to discover instead of tens of thousands to load.</p>
<p>The honest tradeoff:</p>
<ul>
<li><strong>MCP cost</strong>: high token overhead, real setup effort, needs a running server, schema stays in context every session.</li>
<li><strong>MCP benefit</strong>: reaches anything, works across clients, open standard, large ecosystem.</li>
<li><strong>Skill cost</strong>: sandboxed, no network in API, filesystem model can feel weird at first.</li>
<li><strong>Skill benefit</strong>: near-zero idle context cost, fast to author, progressive disclosure scales to dozens of capabilities.</li>
</ul>
<p>The rule I follow now: if the integration does not need a live external system, default to a Skill. Promote to MCP only when you actually need to reach out.</p>
<h2 id="how-do-the-three-stack-up-on-pricing-and-availability">How do the three stack up on pricing and availability?</h2>
<p>Projects are included in the Pro plan at $20 per month, Skills work on Pro and above, and MCP is built into Pro and all higher tiers. Everything except the underlying API usage is available on the cheapest paid plan, but each one has availability quirks worth knowing before you design around them.</p>
<p>Here is a clean breakdown of what ships where.</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Free</th>
<th>Pro ($20/mo)</th>
<th>Max ($100-200/mo)</th>
<th>Team ($20-100/seat)</th>
<th>Enterprise</th>
</tr>
</thead>
<tbody>
<tr>
<td>Projects</td>
<td>Limited</td>
<td>Unlimited</td>
<td>Unlimited</td>
<td>Shared across team</td>
<td>Shared, 500K context</td>
</tr>
<tr>
<td>Skills (custom, Claude.ai)</td>
<td>No</td>
<td>Yes, per user</td>
<td>Yes, per user</td>
<td>Yes, per user</td>
<td>Yes, per user</td>
</tr>
<tr>
<td>Skills (Claude Code)</td>
<td>N/A</td>
<td>Filesystem, free</td>
<td>Filesystem, free</td>
<td>Filesystem, free</td>
<td>Filesystem, free</td>
</tr>
<tr>
<td>Skills (API)</td>
<td>N/A</td>
<td>Via API pricing</td>
<td>Via API pricing</td>
<td>Via API pricing</td>
<td>Via API pricing</td>
</tr>
<tr>
<td>MCP remote connectors</td>
<td>Limited</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Context window</td>
<td>200K</td>
<td>200K (1M on Opus in Code)</td>
<td>200K (1M on Opus in Code)</td>
<td>200K</td>
<td>500K on some models</td>
</tr>
</tbody>
</table>
<p>A few things this table does not capture that will bite you.</p>
<p>Custom Skills do not sync between surfaces. A Skill you upload to claude.ai is not available in the API. A Skill in Claude Code is not available in claude.ai. If you want the same skill in all three, you upload it three times. Anthropic's team has been clear that this is a known gap and not ideal.</p>
<p>Skills on claude.ai are individual per user. There is no org-wide admin distribution today. If you are a Team admin and you want every analyst on your team to use the same compliance-formatting skill, you currently have to tell each of them to upload it themselves. Skills in the Claude API are workspace-wide, so that experience is very different.</p>
<p>MCP servers you add to Claude Desktop or Claude Code live on your machine. Remote MCP servers are available on Pro and above. If you are on Free, your MCP options are limited to what ships built-in.</p>
<p>The API has its own constraints. Skills in the API require three beta headers (<code>code-execution-2025-08-25</code>, <code>skills-2025-10-02</code>, and <code>files-api-2025-04-14</code>), and the sandbox has no network access and no runtime package installation. Plan for that upfront. If your skill depends on making an HTTP call out to a third-party API, it will not work on the API side. You either move that external call to an MCP server, or you pick a different surface.</p>
<h2 id="what-are-the-real-world-limitations-nobody-talks-about">What are the real-world limitations nobody talks about?</h2>
<p>Every one of these three has a sharp edge that the marketing pages do not mention, and you will hit all of them within a month of real use. The honest version. Skills cannot reach the network on the API. MCP burns context whether you use it or not. Projects cannot execute code.</p>
<p>Skills sandbox, laid out plainly:</p>
<ul>
<li>No network access on the Claude API.</li>
<li>No runtime package installation on the Claude API. You get whatever is pre-installed.</li>
<li>Maximum of 8 Skills per API request.</li>
<li>Not covered by Zero Data Retention. Skill definitions and execution data are retained under standard policy.</li>
<li>In Claude Code, skills have full network access, which is great for local use but is also why you should only install skills you wrote or trust.</li>
</ul>
<p>MCP quirks that hurt:</p>
<ul>
<li>Schema cost compounds. Every connected server adds its tool schema to context. Three servers with 30 tools each is close to 100,000 tokens of tool definitions before you send a single user message.</li>
<li>Streamable HTTP at scale still fights horizontal load balancers because sessions are stateful. The 2026 MCP roadmap lists this as a top-priority fix, but it is not solved yet.</li>
<li>Security is real. A malicious MCP server can invoke tools in ways that do not match its stated purpose. The same warning applies to Skills, but MCP has a bigger attack surface because servers can be remote and updated independently.</li>
</ul>
<p>Projects blind spots:</p>
<ul>
<li>No executable code. You cannot run a script inside a project the way a skill can.</li>
<li>Custom instructions are not unlimited. Anthropic explicitly says to keep them concise. Large instruction blocks degrade response quality.</li>
<li>RAG mode kicks in silently when your knowledge base exceeds context. That is good for capacity but bad for determinism. Sometimes the retrieval misses the chunk you needed.</li>
<li>Team collaboration on projects is fine, but you cannot share a single project across organizations. Every team has its own.</li>
</ul>
<p>The combined effect is that each of these tools has a narrow band of things it does better than the others, and a large set of things it does worse. That is why the decision tree at the end of this post matters more than any single-product deep dive.</p>
<h2 id="how-do-you-combine-all-three-in-a-production-workflow">How do you combine all three in a production workflow?</h2>
<p>The production pattern is a Project for context, MCP for external data, and Skills for procedures applied to that data. Anthropic's own team recommends this combined usage, and every team I know who has built real Claude workflows on <a href="https://www.rabinarayanpatra.com/blogs/claude-opus-4-7-release-and-migration-guide">Opus 4.7</a> has converged on the same layering.</p>
<p>Here is how it plays out in a concrete example. Say you are generating a Q3 sales report for a B2B SaaS company. You want the report to pull live numbers, use last quarter's context, and come out in your company's template every time.</p>
<p><strong>Project</strong>: a "Q3 Sales Analysis" project with last quarter's decks, your sales playbook, and custom instructions that tell Claude the audience, the tone, and what to omit.</p>
<p><strong>MCP</strong>: a Salesforce MCP server and a Snowflake MCP server. When you ask for the numbers, Claude calls <code>salesforce_opportunities_list</code> and <code>snowflake_query</code> and gets live data back.</p>
<p><strong>Skill</strong>: a <code>quarterly-report-generator</code> skill. Its SKILL.md says: "When the user asks for a quarterly report, structure the output with these sections, apply the brand styling in <code>brand.md</code>, chart revenue with <code>scripts/chart.py</code>, and format the deck with the pre-built <code>pptx</code> skill."</p>
<p>Ask Claude to "generate the Q3 sales report." The Project gives it the context ("audience is the board, do not include unannounced deals"). MCP gives it the live data ("here are the 47 closed-won deals from last quarter"). The Skill gives it the procedure ("apply the outline, render the charts, export to PowerPoint"). You could not pull that off with any one of the three alone.</p>
<p>A second example, closer to developer work. Code review on pull requests.</p>
<p><strong>Project</strong> (optional): a per-repo project with the architecture notes and the team's coding standards documents.</p>
<p><strong>MCP</strong>: the GitHub MCP server. Claude can read PRs, leave review comments, and check CI status.</p>
<p><strong>Skill</strong>: a <code>pr-review</code> skill that spells out the review checklist, the tone for comments, and how to flag breaking changes versus stylistic nits.</p>
<p>The three-way split matches the three kinds of knowledge you are giving Claude. The Project says what the codebase is about. MCP gives it the keys to actually open files and leave comments. The Skill makes sure every review follows the same standard instead of whatever Claude feels like today.</p>
<p>Rakuten's finance team reported that they reduced report generation from a full day to about an hour by combining MCP data access with Skills for formatting. Not magic. Just the right layering.</p>
<h2 id="which-one-should-you-actually-use">Which one should you actually use?</h2>
<p>If you are still deciding, use this tree. It sounds dumb when written out, but it has saved me from picking the wrong tool at least a dozen times.</p>
<p><strong>Does Claude need to reach a system that is not on its local machine or sandbox?</strong></p>
<ul>
<li>Yes. Use MCP. Connect the system. If there is already a community MCP server for it, use that. If not, write one. This is the only path.</li>
<li>No. Continue.</li>
</ul>
<p><strong>Is the thing I am teaching Claude specific to one body of work, like a codebase or a client engagement?</strong></p>
<ul>
<li>Yes. Put it in a Project. Upload the docs, write the instructions, keep it scoped.</li>
<li>No. Continue.</li>
</ul>
<p><strong>Is it a procedure I want Claude to apply consistently across contexts?</strong></p>
<ul>
<li>Yes. Make it a Skill. Write the SKILL.md, include helper scripts if needed, and let it load on demand.</li>
<li>No. It is probably just a one-off prompt. Do not overbuild.</li>
</ul>
<p>Three questions, one answer at the end, every time. If the answer is "more than one of these," you are in the combined-workflow case from the previous section.</p>
<p>A quick note on what I would not recommend. Do not use a Project for your company's style guide unless the style guide only applies to that project. Do not use an MCP server for a local script you could wrap in a Skill. Do not use a Skill to replace live data access. These are the three most common mistakes I see, and they all come from reaching for the wrong tool first.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The short version of everything above: Projects hold context, Skills teach procedures, MCP reaches external systems. Most developers I talk to are using one of the three and ignoring the other two. The best setups I have seen use all three, with a clear mental model of which layer owns which job.</p>
<p>My own view after a few months of real use. Skills are the quietly winning primitive. They are cheap to write, cheap to load, portable across surfaces in ways Projects never will be, and they solve the single biggest pain point I had with MCP, which was paying for capability I was not using. Progressive disclosure is genuinely the right architecture for agent knowledge. I expect most of what people are currently bolting onto MCP servers to migrate into Skills over the next year, with MCP staying as the system-of-record for connectivity rather than procedure. Worth watching which way Anthropic's own pre-built skills go next, because that signals which workflows they think should live inside Skills and which should stay as tools.</p>
<p>For the official reference on each, see the <a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview">Agent Skills documentation</a>, the <a href="https://modelcontextprotocol.io/docs/learn/architecture">Model Context Protocol architecture overview</a>, and Anthropic's own <a href="https://claude.com/blog/skills-explained">Skills explained</a> post comparing all five primitives. The <a href="https://blog.modelcontextprotocol.io/posts/2026-mcp-roadmap/">2026 MCP roadmap</a> is also worth reading if you care about where the protocol is heading.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/claude-mythos-project-glasswing">Claude Mythos and Project Glasswing</a> — Anthropic's gated frontier model release, and what it means for how Claude capabilities are distributed.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/my-thoughts-on-vibe-coding-2025">My Thoughts on Vibe Coding</a> — how I actually use AI coding tools day to day, and where Skills fit into that flow.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/google-antigravity">Google Antigravity: Why I Think It Changes Everything</a> — the other big agentic platform story, and the closest competitor to this stack.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a Modern Documentation Generator with Next.js 16</a> — the kind of personal project where a custom Skill pays off fast.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Amazon S3 Files: Mount S3 as NFS (Setup + Cost)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/aws-s3-2026-files-vectors-and-beyond</link>
      <guid>https://www.rabinarayanpatra.com/blogs/aws-s3-2026-files-vectors-and-beyond</guid>
      <pubDate>Thu, 09 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[S3 Files lets you mount S3 buckets via NFS with sub-millisecond latency. Here is how it works, what it costs, and why AWS also killed SSE-C by default.]]></description>
      <content:encoded><![CDATA[<p>I've been running EFS-to-S3 sync jobs for two years. Cron schedules, lifecycle policies, rsync scripts that break every time someone changes a directory structure. All because S3 couldn't speak file system.</p>
<p>That changed this week.</p>
<p>On April 7, 2026, AWS announced <a href="https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/">Amazon S3 Files</a>, a feature that lets you mount any S3 bucket as a shared NFS file system. No gateway. No third-party tool. No data copies. Your applications read and write files, and S3 stores them. That's it.</p>
<p>And quietly, on April 6, AWS also started <a href="https://aws.amazon.com/about-aws/whats-new/2026/04/s3-default-bucket-security-setting/">disabling SSE-C encryption by default</a> on all new S3 buckets. If you're managing encryption keys manually, that one needs your attention too.</p>
<p>Let me walk through both changes.</p>
<h2 id="what-is-amazon-s3-files-and-how-does-it-work">What is Amazon S3 Files and how does it work?</h2>
<p>Amazon S3 Files gives S3 buckets a fully-featured file system interface using NFS v4.2. You can mount a bucket on EC2, Lambda, EKS, ECS, Fargate, or AWS Batch and interact with your data using standard file operations: <code>open()</code>, <code>read()</code>, <code>write()</code>, <code>ls</code>, <code>cp</code>, <code>mv</code>. No SDK. No API calls. Just a mount point.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/s3-files-architecture.webp" alt="S3 Files Architecture Overview - Source: AWS Blog" width="1024" height="596">
<em>Architecture overview of Amazon S3 Files. Source: <a href="https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/">AWS News Blog</a></em></p>
<p>S3 is now the first and only cloud object store that provides native file system access while keeping all data in object storage. Your objects don't move to a separate file system. They stay in S3 with all the durability, lifecycle policies, and access controls you already have.</p>
<p>S3 Files is generally available in 34 AWS Regions as of launch day.</p>
<h3 id="the-stage-and-commit-model">The stage and commit model</h3>
<p>This is the part that surprised me. Instead of translating every file write into an immediate S3 PUT, S3 Files batches changes and commits them to S3 roughly every 60 seconds. AWS borrowed this concept from version control.</p>
<p>Here's what that means in practice:</p>
<ol>
<li>You write a file through the NFS mount</li>
<li>The write lands in a caching layer (built on EFS infrastructure)</li>
<li>S3 Files aggregates writes within a 60-second window</li>
<li>Multiple writes to the same file become a single S3 PUT</li>
<li>The committed object appears in S3 with full consistency</li>
</ol>
<p>This batching has two practical benefits. First, it cuts your S3 request costs because ten rapid writes to the same file become one PUT, not ten. Second, if you're using S3 versioning, you don't end up with ten versions of a file that changed in under a minute.</p>
<p>But it also means there's a ~60-second lag before file changes are visible on the S3 side. If your workflow needs immediate S3 API visibility of written data, you need to account for that delay.</p>
<p>When there's a conflict (say, someone writes to a file through NFS while another process updates the same object via the S3 API), S3 remains authoritative. The file-side version gets moved to a <code>lost+found</code> directory with metrics for visibility. No silent data loss.</p>
<h3 id="the-caching-layer-under-the-hood">The caching layer under the hood</h3>
<p>When you create an S3 Files file system, AWS provisions a caching layer backed by EFS infrastructure. This cache holds three things:</p>
<ul>
<li><strong>Recently read files</strong>: Hot reads come from cache with sub-millisecond latency</li>
<li><strong>Recently written files</strong>: Staged writes waiting for the next commit cycle</li>
<li><strong>Metadata</strong>: Directory listings, file attributes, timestamps</li>
</ul>
<p>Small file reads are served entirely from the cache. Large file reads (over 1MB) stream directly from S3 and don't incur S3 Files charges. The aggregate read throughput can reach multiple terabytes per second.</p>
<p>This design is what makes S3 Files cost-effective. You pay file system rates ($0.30/GB-month) only on the data that's actively cached. A petabyte bucket where only 500GB is actively read? You're paying S3 rates for the petabyte and file system rates for the 500GB.</p>
<h2 id="how-do-you-set-up-s3-files-on-an-ec2-instance">How do you set up S3 Files on an EC2 instance?</h2>
<p>The setup is straightforward. Here's the full flow:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Step 1: Create an S3 Files file system linked to your bucket</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">aws</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> s3api</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> create-file-system</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --bucket</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-data-bucket</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --file-system-name</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-fs</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Step 2: Get the mount target DNS name</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">aws</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> s3api</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> describe-file-system</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --bucket</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-data-bucket</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --file-system-name</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-fs</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">FileSystem.MountTargets[0].DnsName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --output</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> text</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Step 3: Mount on your EC2 instance</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">sudo</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> mount</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -t</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> nfs4</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  -o</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> nfsvers=4.2,rsize=1048576,wsize=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1048576</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  fs-mount-target.efs.us-east-1.amazonaws.com:/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  /mnt/s3data</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Step 4: Use it like any filesystem</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /mnt/s3data</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">cp</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> local-file.csv</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /mnt/s3data/uploads/</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">cat</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /mnt/s3data/reports/quarterly.json</span></span></code></pre></figure>
<p>Once mounted, every application on that instance can access S3 data through normal file I/O. Python scripts, Java apps, shell scripts, legacy C++ binaries. Nothing needs to know it's talking to S3.</p>
<p>For persistent mounts across reboots, add it to <code>/etc/fstab</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># /etc/fstab entry for S3 Files</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">fs-mount-target.efs.us-east-1.amazonaws.com:/</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /mnt/s3data</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> nfs4</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> nfsvers=4.2,rsize=1048576,wsize=1048576,_netdev</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span></span></code></pre></figure>
<h2 id="where-does-s3-files-beat-efs-and-where-does-it-not">Where does S3 Files beat EFS (and where does it not)?</h2>
<p>This is the question I've been testing all week. S3 Files isn't a drop-in EFS replacement for every workload, but for specific patterns it's clearly better.</p>
<h3 id="s3-files-wins">S3 Files wins</h3>
<table>
<thead>
<tr>
<th>Scenario</th>
<th>Why S3 Files</th>
</tr>
</thead>
<tbody>
<tr>
<td>Large datasets, small active working set</td>
<td>Pay S3 rates on cold data, file system rates only on hot data</td>
</tr>
<tr>
<td>Legacy app migration to S3</td>
<td>Zero code changes needed, just mount and go</td>
</tr>
<tr>
<td>AI/ML training data pipelines</td>
<td>Read training data as files, store as S3 objects</td>
</tr>
<tr>
<td>Agentic AI workloads</td>
<td>Shared workspace across multiple compute instances</td>
</tr>
<tr>
<td>Multi-service data sharing</td>
<td>Multiple EKS pods or Lambda functions reading the same dataset</td>
</tr>
</tbody>
</table>
<h3 id="efs-still-wins">EFS still wins</h3>
<table>
<thead>
<tr>
<th>Scenario</th>
<th>Why EFS</th>
</tr>
</thead>
<tbody>
<tr>
<td>All data is hot, constant read/write</td>
<td>EFS avoids the commit delay and S3 request overhead</td>
</tr>
<tr>
<td>Sub-second write visibility needed</td>
<td>S3 Files has a ~60-second commit lag</td>
</tr>
<tr>
<td>Windows workloads (SMB)</td>
<td>S3 Files only supports NFS, no SMB</td>
</tr>
<tr>
<td>Hard link requirements</td>
<td>S3 Files doesn't support hard links</td>
</tr>
<tr>
<td>Bucket exceeds 50 million objects</td>
<td>AWS warns about performance at this scale</td>
</tr>
</tbody>
</table>
<h3 id="pricing-comparison">Pricing comparison</h3>
<p>The pricing math depends entirely on your access pattern:</p>
<ul>
<li><strong>S3 Files cached storage</strong>: $0.30/GB-month (only for actively cached data)</li>
<li><strong>S3 Files reads (small files)</strong>: $0.03/GB from cache</li>
<li><strong>S3 Files reads (large files, 1MB+)</strong>: $0 from S3 Files (standard S3 GET charges apply)</li>
<li><strong>S3 Files writes</strong>: $0.06/GB</li>
</ul>
<p>Compare that to EFS Performance-optimized at $0.30/GB for standard storage and $0.03/GB for reads. The difference shows up at scale: if you have 10TB in a bucket but only touch 200GB regularly, S3 Files costs a fraction of what an equivalent EFS setup would cost.</p>
<h2 id="what-are-the-gotchas-you-should-know-before-adopting-s3-files">What are the gotchas you should know before adopting S3 Files?</h2>
<p>I ran into a few things during my initial testing that are worth flagging.</p>
<p><strong>The 60-second commit window is real.</strong> If you write a file via NFS and immediately try to read it through the S3 API (using <code>aws s3 cp</code> or a direct GET), it won't be there yet. Your application logic needs to handle this. For workflows that do writes via NFS and reads via S3 API, consider adding a short wait or checking for object existence.</p>
<p><strong>NFS file locks don't protect against S3 API access.</strong> If you lock a file through NFS, that lock only applies to other NFS clients. Someone using the S3 API directly can still modify the object. This isn't a bug. It's how the boundary between file system and object store works. But it can bite you if mixed access isn't on your radar.</p>
<p><strong>The 50-million object warning is something to watch.</strong> AWS recommends caution when a mounted bucket contains more than 50 million objects. Directory listings and metadata operations can slow down at that scale. If you're dealing with buckets that large, consider using S3 prefixes to scope your mount.</p>
<p><strong>No pNFS, Kerberos, or nconnect support.</strong> If your NFS setup depends on parallel NFS, Kerberos authentication, NFSv4 data retention, or the <code>nconnect</code> mount option, those aren't available yet at GA. Standard NFS v4.2 features work fine.</p>
<p><strong>SMB is not supported.</strong> Windows workloads that need file system access to S3 still need FSx or a gateway solution.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/aws-s3-2026-files-vectors-and-beyond/s3-files-comparison.webp" alt="S3 Files Technical Comparison Architecture" width="1024" height="1024"></p>
<h2 id="why-did-aws-disable-sse-c-encryption-by-default">Why did AWS disable SSE-C encryption by default?</h2>
<p>This change flew under the radar next to S3 Files, but it affects every new bucket created after April 6, 2026.</p>
<p>SSE-C (Server-Side Encryption with Customer-Provided Keys) lets you bring your own encryption key on every PUT and GET request. S3 encrypts and decrypts using your key but never stores it. The idea is maximum control. The reality is operational risk. Lose the key, lose the data forever. AWS can't recover it for you. There's no "forgot my password" option.</p>
<p>AWS KMS solved this years ago with customer-managed keys (CMKs) that give you full ownership and control, plus key rotation, auditing through CloudTrail, and recovery options. For most workloads, KMS does everything SSE-C does, minus the footgun.</p>
<p>So AWS made SSE-C opt-in instead of opt-out. Here's how the rollout works:</p>
<h3 id="what-changes-for-new-buckets">What changes for new buckets</h3>
<p>Every new general-purpose S3 bucket created after April 6, 2026 has SSE-C disabled by default. If you try to upload an object with SSE-C headers, you'll get an access denied error unless you explicitly enable SSE-C first.</p>
<p>To enable SSE-C on a new bucket:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Explicitly allow SSE-C on a new bucket</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">aws</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> s3api</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> put-bucket-encryption</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --bucket</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-new-bucket</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5">  --server-side-encryption-configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">    "Rules": [{</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">      "ApplyServerSideEncryptionByDefault": {</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">        "SSEAlgorithm": "AES256"</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">      },</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">      "BucketKeyEnabled": false</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">    }],</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">    "AllowSSEC": true</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">  }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span></code></pre></figure>
<h3 id="what-changes-for-existing-buckets">What changes for existing buckets</h3>
<p>This is the part that might catch people off guard. AWS is also disabling SSE-C on existing buckets that have <strong>zero SSE-C encrypted objects</strong>. If you created a bucket, never used SSE-C, but your automation code includes SSE-C headers "just in case," those writes will start failing.</p>
<p>Existing buckets that actually contain SSE-C objects? No changes. AWS won't touch those.</p>
<h3 id="who-this-affects">Who this affects</h3>
<p>If you're using AWS KMS (SSE-KMS) or S3-managed keys (SSE-S3) for encryption, this change does nothing to you. Your buckets already don't use SSE-C.</p>
<p>If you're one of the teams still on SSE-C, you'll want to:</p>
<ol>
<li>Audit which buckets actually use SSE-C (<code>aws s3api get-bucket-encryption</code>)</li>
<li>Plan a migration to KMS for buckets that don't strictly need SSE-C</li>
<li>Explicitly re-enable SSE-C on new buckets where it's genuinely required</li>
</ol>
<p>The rollout covers 37 AWS Regions, including GovCloud and China regions, and will complete over the next few weeks.</p>
<h2 id="what-do-these-changes-tell-us-about-where-s3-is-heading">What do these changes tell us about where S3 is heading?</h2>
<p>If I look at S3 Files and the SSE-C default together, they tell the same story: AWS is reducing the reasons you'd reach for anything other than S3.</p>
<p>Need file system access? You used to need EFS plus sync scripts. Now you mount S3 directly. Need encryption with your own keys? You used to reach for SSE-C. Now AWS is steering you toward KMS, which handles key management for you.</p>
<p>S3 now stores over 500 trillion objects and handles roughly 200 million requests per second. It turned 20 years old last month. And instead of letting it coast, AWS gave it the most significant capability upgrade since S3 Intelligent-Tiering launched in 2018.</p>
<p>For my own projects, I'm already replacing two EFS-backed data pipelines with S3 Files mounts. The sync cron jobs are gone. The drift alerts are gone. One mount point, one storage bill, and a 60-second commit window I can easily live with.</p>
<p>If you've been running parallel storage systems just to get file access to your S3 data, this week is a good week to rethink that architecture.</p>
<p>For the full details, see the <a href="https://aws.amazon.com/blogs/aws/launching-s3-files-making-s3-buckets-accessible-as-file-systems/">official S3 Files announcement</a>, the <a href="https://aws.amazon.com/s3/features/files/">S3 Files product page</a>, and the <a href="https://aws.amazon.com/blogs/storage/advanced-notice-amazon-s3-to-disable-the-use-of-sse-c-encryption-by-default-for-all-new-buckets-and-select-existing-buckets-in-april-2026/">SSE-C security default announcement</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-Driven Anomaly Detection for Security</a> - How cloud infrastructure and AI work together for real-time threat detection.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">Implementing the Outbox Pattern with CDC in Microservices</a> - Storage design patterns that affect reliability in distributed systems.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet">The Day a React Patch Broke the Internet</a> - Another deep dive into an infrastructure event that caught everyone off guard.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Claude Mythos and Project Glasswing: Anthropic's Locked-Down AI Model]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/claude-mythos-project-glasswing</link>
      <guid>https://www.rabinarayanpatra.com/blogs/claude-mythos-project-glasswing</guid>
      <pubDate>Wed, 08 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Anthropic's new Claude Mythos model scores 93.9% on SWE-bench Verified and finds zero-days autonomously. Here is why it is not getting a public release.]]></description>
      <content:encoded><![CDATA[<p>Anthropic shipped a new Claude model on April 7, 2026 and decided you cannot use it. Not as an API customer, not on Claude.ai, not through any normal channel. They built something called Claude Mythos, claimed it can autonomously find zero-days in browsers and operating systems, and then locked it behind a program called Project Glasswing.</p>
<p>This is the first time Anthropic has launched a frontier model as a deliberate non-product. I have been watching frontier launches for a while now, and the framing on this one is genuinely different from anything OpenAI, Google, or even earlier Anthropic releases have done. So let me walk through what Mythos actually is, what the numbers say, and why I think the gated rollout is the most interesting part of the story.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-mythos-project-glasswing/data-center.webp" alt="A modern enterprise data center corridor with illuminated server racks" width="1024" height="1024"></p>
<h2 id="what-is-claude-mythos-and-how-is-it-different-from-claude-opus">What is Claude Mythos and how is it different from Claude Opus?</h2>
<p>Claude Mythos is Anthropic's most capable model to date, released on April 7, 2026 as a gated research preview rather than a general product. Anthropic describes it as a new model class with state-of-the-art performance on agentic coding, complex reasoning, and offensive security tasks.</p>
<p>The headline is not really the benchmarks, although the benchmarks are huge. The headline is the positioning. Claude Opus, Sonnet, and Haiku are products. They have pricing pages. They have rate limits you can buy your way out of. Mythos is none of those things. It is closer to how a national lab would describe a piece of dual use research than how a SaaS company describes a model.</p>
<p>Here is what Anthropic actually claims Mythos can do:</p>
<ul>
<li>Reverse engineer closed source binaries and find exploitable bugs without human guidance.</li>
<li>Develop working exploits against real targets, including the Firefox JavaScript engine, where it produced working exploits 181 times in their internal evaluation.</li>
<li>Find high severity vulnerabilities in every major operating system and every major browser during pre-release testing.</li>
<li>Saturate most of the public cybersecurity benchmarks Anthropic used for prior models, which is why they had to move to novel real world tasks.</li>
</ul>
<p>That last point is the one I keep coming back to. When a model saturates the eval suite, the eval suite stops being useful. You start needing real targets, and real targets are exactly what you do not want an unrestricted model running against.</p>
<h2 id="how-does-claude-mythos-perform-on-benchmarks-compared-to-opus-46">How does Claude Mythos perform on benchmarks compared to Opus 4.6?</h2>
<p>Claude Mythos pulls a double digit lead over Claude Opus 4.6 on every benchmark Anthropic published, with the largest gaps on security and agentic coding tasks. Here are the numbers from Anthropic's own evaluation post:</p>
<table>
<thead>
<tr>
<th>Benchmark</th>
<th>Claude Opus 4.6</th>
<th>Claude Mythos Preview</th>
<th>Delta</th>
</tr>
</thead>
<tbody>
<tr>
<td>SWE-bench Verified</td>
<td>80.8%</td>
<td>93.9%</td>
<td>+13.1</td>
</tr>
<tr>
<td>SWE-bench Pro</td>
<td>53.4%</td>
<td>77.8%</td>
<td>+24.4</td>
</tr>
<tr>
<td>CyberGym (vuln reproduction)</td>
<td>66.6%</td>
<td>83.1%</td>
<td>+16.5</td>
</tr>
</tbody>
</table>
<p>A few things stand out. SWE-bench Verified at 93.9% is essentially the ceiling of the benchmark. There are known label issues and ambiguous tasks in the remaining 6%, so any further gains are noise. SWE-bench Pro is the harder, larger, more realistic version, and a 24 point jump there is the kind of move you usually see across two model generations, not one. CyberGym is the one that actually matters for the Glasswing framing. It measures whether a model can reproduce real CVEs end to end, and 83.1% is the first time any public number has cleared 80% on that benchmark.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/claude-mythos-project-glasswing/benchmark-results.webp" alt="Printed benchmark results on a matte black desk" width="1024" height="1024"></p>
<p>Numbers aside, the qualitative claim is the more interesting one. Anthropic says Mythos is the first model where the bottleneck on offensive security work is no longer the model itself, it is the human reviewer triaging what the model finds. That is a real shift. For the last two years the joke among security people has been that LLMs are great at writing CVE descriptions and bad at finding actual bugs. Mythos is the first launch that seriously contests that.</p>
<h2 id="why-is-anthropic-gating-mythos-behind-project-glasswing-instead-of-shipping-it">Why is Anthropic gating Mythos behind Project Glasswing instead of shipping it?</h2>
<p>Anthropic is not making Mythos generally available because they believe a model that can autonomously find and exploit zero-days in production software is too dangerous to put behind a credit card. Project Glasswing is the structure they built so the model can still do useful defensive work without getting handed to anyone who signs up.</p>
<p>The shape of Glasswing, based on Anthropic's own announcement and the partner list:</p>
<ul>
<li>12 launch partners, including Microsoft, Nvidia, Cisco, Amazon, Apple, and Google.</li>
<li>40+ additional organizations that build or maintain critical software infrastructure, granted gated access.</li>
<li>100 million dollars in Mythos usage credits committed across the program.</li>
<li>4 million dollars in direct donations to open source security organizations.</li>
<li>Access through Amazon Bedrock under a separate gated research preview agreement.</li>
</ul>
<p>Reading between the lines, Glasswing is doing three things at once. It is a safety story, it is a moat, and it is a regulatory positioning move. The safety story is the obvious one. The moat is that the partners getting Mythos access are exactly the companies whose own security posture matters most to Anthropic's enterprise pipeline. The regulatory move is that by self-imposing a restriction this aggressive, Anthropic gets to show up at every AI safety hearing for the next year with a concrete example of voluntary risk mitigation.</p>
<p>I do not think any of those motives are bad. I just think it is worth naming all three instead of pretending the launch is purely altruistic.</p>
<h2 id="what-does-claude-mythos-mean-for-developers-who-will-never-touch-it">What does Claude Mythos mean for developers who will never touch it?</h2>
<p>Even if you cannot use Mythos directly, the launch tells you three things about where frontier models are heading and what to plan for over the next 12 months.</p>
<p><strong>One. The era of model launches as pure product launches is ending.</strong> Mythos is the first frontier release I can remember where the headline is not pricing or context window, it is a refusal to ship. Expect more of this. The next time a lab crosses a capability line that scares its own safety team, the playbook is now public: announce, gate, partner, donate, move on.</p>
<p><strong>Two. SWE-bench is cooked as a useful number.</strong> When the top model is at 93.9% Verified and 77.8% Pro, the benchmark stops separating models from each other. If you write about LLMs or ship developer tooling, you need to start citing CyberGym, Aider polyglot, real bug bounty results, and end-to-end agent task completions instead. SWE-bench is going to look like GLUE looked in 2020 within a year.</p>
<p><strong>Three. Defensive tooling is about to get strange.</strong> If Mythos and its successors are running inside Microsoft, Apple, and Google, the rate at which serious vulnerabilities get found and patched is going to accelerate in a way that is genuinely good for everyone, but it also means the half-life of an unpatched bug in popular software is going to collapse. If you maintain an open source library, you should be thinking right now about how fast you can credibly turn around a security advisory, because the discovery side is no longer the slow step.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Claude Mythos is the first time a frontier lab has shipped a model by deciding not to ship it, and the framing matters more than the numbers. The benchmarks are real, the capabilities are real, and the gating is real, but the thing to actually pay attention to is the new shape of the launch itself. Frontier releases are starting to look less like product drops and more like controlled disclosures.</p>
<p>If you are building anything that touches security, the practical move this week is to go read the CyberGym methodology and the Glasswing partner list, then ask yourself which side of that list your product lives on. That is the question Anthropic just made everyone answer.</p>
<p>For more on the launch, see Anthropic's <a href="https://www.anthropic.com/glasswing">Project Glasswing announcement</a>, the <a href="https://red.anthropic.com/2026/mythos-preview/">Mythos Preview cybersecurity evaluation</a>, Simon Willison's <a href="https://simonwillison.net/2026/Apr/7/project-glasswing/">analysis of the gated release</a>, and the <a href="https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-bedrock-claude-mythos/">AWS Bedrock availability note</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/google-antigravity">Google Antigravity and the New AI Coding Stack</a> — How another lab is approaching agentic coding from a very different angle.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI Driven Anomaly Detection for Security</a> — The defensive side of the same trend Mythos accelerates.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/agentic-payments-razorpay-npci-upi">Agentic Payments on Razorpay, NPCI, and UPI</a> — What gated, high-trust AI deployments look like in a regulated domain.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Why I Replaced useEffect Data Fetching with Server Actions]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/replacing-useeffect-data-fetching-server-actions</link>
      <guid>https://www.rabinarayanpatra.com/blogs/replacing-useeffect-data-fetching-server-actions</guid>
      <pubDate>Tue, 07 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[I ripped useEffect data fetching out of my Next.js 16 app and replaced it with Server Components and Server Actions. Here is what broke and what got better.]]></description>
      <content:encoded><![CDATA[<p>I opened my Next.js app one morning and counted 23 <code>useEffect</code> blocks whose only job was to fetch data on mount. Dashboards, profile pages, comment threads, the chatbot history view. Every one of them followed the same sad pattern: render nothing, flash a spinner, wait for the client to wake up, fire a request, set state, re-render. It worked. It also felt wrong in a way I had been ignoring for months.</p>
<p>So I spent a weekend ripping all of it out. Server Components handle the reads now. Server Actions handle the writes. I kept <code>useEffect</code> around for exactly the things it was designed for, and nothing else. This is the refactor diary, including the parts where I broke things and had to back out.</p>
<h2 id="why-was-useeffect-data-fetching-the-wrong-default">Why was useEffect data fetching the wrong default?</h2>
<p>Because it forces the browser to do work the server could have already finished. The classic <code>useEffect(() => { fetch(...) }, [])</code> pattern ships empty HTML, boots React on the client, runs the effect, opens a network request, parses JSON, then finally paints the thing the user came for. On a fast laptop nobody notices. On a mid-range Android phone on a train in Bhubaneswar, it is a full second of staring at a spinner.</p>
<p>There are three specific problems I kept hitting.</p>
<p>The first is the waterfall. A page with three <code>useEffect</code> fetches inside three different components ends up making three sequential round trips after hydration finishes. Each one waits for the component above it to mount. I had a profile page where the avatar, the stats card, and the recent activity list were fetched this way. Total time to interactive on a throttled connection was over three seconds, and the API itself responded in under 150ms each.</p>
<p>The second is the flash of nothing. You need a <code>loading</code> state, an <code>error</code> state, a <code>data</code> state, and a guard for the unmounted case. I wrote the same <code>if (loading) return &#x3C;Skeleton /></code> branch so many times I turned it into a hook, then turned that hook into a bigger hook, and eventually realised I had built a worse version of React Query without meaning to.</p>
<p>The third is SEO and AI crawlers. Google renders JavaScript, but it does not love it. ChatGPT's crawler and Perplexity's crawler are even less patient. If the meat of your page only appears after a client fetch, you are trusting a lot of bots to stick around. On this portfolio I care about that, because half my traffic comes from people asking an AI a question.</p>
<h2 id="how-do-server-components-actually-replace-the-fetch-on-mount-pattern">How do Server Components actually replace the fetch-on-mount pattern?</h2>
<p>Server Components let you <code>await</code> your data directly in the component body, on the server, before any HTML reaches the browser. There is no hook, no state, no effect. The component is an async function, and whatever it returns is already resolved by the time the user sees it.</p>
<p>Here is the before and after from my activity feed. This is real code from the refactor, trimmed for the post.</p>
<p><strong>Old client component with useEffect:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">use client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> useEffect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> useState</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">react</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ActivityFeed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> setItems</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Activity</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">[] </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">|</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> setError</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">  useEffect</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    let</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cancelled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    fetch</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/activity?userId=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">then</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> r</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">then</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cancelled</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setItems</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">data</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">catch</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cancelled</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setError</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">e</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cancelled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">])</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">error</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">ErrorCard</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">items</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">FeedSkeleton</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">FeedList</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>New Server Component:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> getActivity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/lib/activity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> FeedList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">./feed-list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ActivityFeed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getActivity</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">FeedList</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That is the whole component. No hook, no state, no cancellation flag, no skeleton branch inside the component itself. The skeleton now lives in a <code>loading.tsx</code> file next to the route, and <a href="https://www.rabinarayanpatra.com/snippets/nextjs/streaming-suspense-loading">React Suspense streams the real content in when it is ready</a>. The error case lives in <code>error.tsx</code>. Both of those are built into the App Router, and I had been ignoring them because my brain was still wired for the old pattern.</p>
<p>The nicest part is that <code>getActivity</code> is now a plain async function that talks to the database directly. No API route, no serializer, no extra network hop. The database call and the page render happen in the same process, on the same machine, inside the same request.</p>
<h2 id="where-do-server-actions-fit-once-reads-move-to-the-server">Where do Server Actions fit once reads move to the server?</h2>
<p>Server Actions handle the write half of the story. When the user clicks Like, posts a comment, or updates a setting, I used to <code>fetch('/api/comments', { method: 'POST' })</code> from inside a <code>useEffect</code> or a submit handler, then manually refetch the list. Now the mutation is a function with <code>'use server'</code> on top, and the page revalidates itself.</p>
<p>Here is the comment form, before and after.</p>
<p><strong>Old pattern:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">use client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetch</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/comments</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> JSON</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // now manually refetch the list, or reload the page, or</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // hope a SWR mutate call covers it</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>New pattern:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// app/posts/[slug]/actions.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">use server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> revalidateTag</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next/cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> db</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/lib/db</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> addComment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">postId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> formData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> FormData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> String</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">formData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">??</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">trim</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">text</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Empty comment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  await</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> db</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">comment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">  revalidateTag</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">comments:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">postId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="tsx" data-theme="material-theme github-light"><code data-language="tsx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// comment-form.tsx</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">use client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> useActionState</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">react</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> addComment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">./actions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CommentForm</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> postId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F07178;--shiki-light:#E36209"> postId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">state</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> formAction</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> useActionState</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    addComment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">bind</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postId</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    {</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">form</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> action</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">formAction</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">textarea</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> required</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">button</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> disabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pending</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pending </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Posting...</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> :</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">button</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">state</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">error </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">p</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">text-red-500</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>{</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">state</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">form</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>No <code>useEffect</code>. No manual refetch. <code>revalidateTag</code> tells Next.js that anything tagged <code>comments:${postId}</code> is stale, and the next render of the Server Component that reads those comments will pick up the new row. The pending and error state come from <code>useActionState</code>, which is a proper React hook that knows about the action's lifecycle.</p>
<p>This is where the mental model finally clicked for me. Reads live on the server and are cached per route or per tag. Writes go through Server Actions and invalidate tags. The client is only responsible for the things the client is actually good at: typing, clicking, and showing pending UI.</p>
<h2 id="what-broke-during-the-refactor">What broke during the refactor?</h2>
<p>Three things broke, and I want to be honest about them because every "rewrite everything" post pretends the rewrite was smooth.</p>
<p><strong>Search and filter state stopped working the old way.</strong> I had a search input that used <code>useEffect</code> to refetch results whenever the query changed. Moving the list to a Server Component meant the query had to live somewhere the server could see. I moved it into the URL as a search param, and now the Server Component reads <code>await searchParams</code> and refetches on every URL change. This is better in the long run (shareable URLs, back button works, no client state to sync) but it took a morning to rewire every filter and <a href="https://www.rabinarayanpatra.com/snippets/react/use-debounce">debounce the input with a typed <code>useDebounce</code> hook</a> without re-introducing a <code>useEffect</code>.</p>
<p><strong>Optimistic updates got harder before they got easier.</strong> The old pattern let me shove the new comment into local state instantly and reconcile later. With Server Actions you get <code>useOptimistic</code> for this, and it works well, but the API is new and I had to read the docs twice. The first version of my comment list flickered because I was updating the optimistic state inside the wrong component. Once I moved it to the list component itself, it was smoother than the old code.</p>
<p><strong>I accidentally broke streaming on one route.</strong> I wrapped a Server Component in a client boundary by mistake, which forced the whole subtree to render on the client and killed the streaming behavior. The fix was to push <code>'use client'</code> down to the actual interactive leaf (a button), not the card that contained it. The rule I repeat to myself now is: the client boundary goes on the smallest thing that needs it.</p>
<p>None of these were dealbreakers. All of them were things my old <code>useEffect</code>-everywhere code had papered over by being uniformly mediocre.</p>
<h2 id="when-do-i-still-reach-for-useeffect">When do I still reach for useEffect?</h2>
<p>I still use <code>useEffect</code> whenever the work is genuinely browser-only. Reading from <code>localStorage</code> for a theme preference. Subscribing to a <code>matchMedia</code> query. Wiring up an IntersectionObserver for a scroll reveal. Mounting a third-party chart library that touches the DOM directly. Syncing a piece of state to the URL hash. None of that belongs on the server, and pretending otherwise leads to weird hydration errors.</p>
<p>The rule I landed on is short enough to put on a sticky note. If the effect is fetching data, it should probably be a Server Component. If the effect is touching the browser, it is still a <code>useEffect</code>. Everything else is a judgment call, and the judgment usually lands on the server side now.</p>
<h2 id="was-the-refactor-worth-it">Was the refactor worth it?</h2>
<p>Yes, and I was not expecting the numbers to be this clear. On my profile page, time to first contentful paint dropped from around 1.4s to 380ms on a simulated 4G connection. The JavaScript bundle for that route shrank by 31KB because I deleted the client-side fetch helpers and their tiny useEffect wrappers. The code itself is shorter: the activity feed alone went from 47 lines to 9.</p>
<p>The part I did not expect was how much calmer the code felt to read afterwards. A Server Component is just an async function that returns JSX. There is no lifecycle to reason about, no cancellation token, no stale closure traps. When something breaks, the stack trace points at the line that actually broke, not at a <code>useEffect</code> three components up that fired in the wrong order.</p>
<p>If you are still writing <code>useEffect(() => { fetch(...) }, [])</code> in a new Next.js 16 project, I would push back on that default. The App Router has been stable long enough, the Server Actions API is no longer experimental, and the tooling around Suspense and streaming finally feels finished. The only real cost of moving is a few afternoons spent unlearning habits from the Pages Router era.</p>
<p>For more on the patterns in this post, see the <a href="https://nextjs.org/docs/app/getting-started/server-and-client-components">Next.js Server Components docs</a>, the <a href="https://nextjs.org/docs/app/getting-started/updating-data">Server Actions and Mutations guide</a>, and Dan Abramov's <a href="https://react.dev/learn/you-might-not-need-an-effect">You Might Not Need an Effect</a> from the official React docs.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">Hello proxy.ts: the Next.js 16 middleware rename</a> — Another Next.js 16 habit you probably need to unlearn.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a Modern Docs Generator with Next.js 16</a> — How I use Server Components and streaming for a real content-heavy app.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs">Full-Stack Social Identity with Spring and Next.js</a> — Where Server Actions meet a real backend.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet">The Day a React Patch Broke the Internet</a> — Why I trust the React team but still read every changelog twice.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Tail-Call Optimization Explained: Why Recursion Doesn't Have to Blow Your Stack]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/tail-call-optimization-explained</link>
      <guid>https://www.rabinarayanpatra.com/blogs/tail-call-optimization-explained</guid>
      <pubDate>Sat, 04 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Learn how tail-call optimization lets you recurse without stack overflows. Covers TCO in Elixir, Scala, Clojure, and why Java still doesn't support it.]]></description>
      <content:encoded><![CDATA[<p>I hit a <code>StackOverflowError</code> for the first time back in college. I was writing a recursive Fibonacci function in Java, and it worked fine for small inputs. Then I tried <code>fib(50000)</code> and my JVM just died. That's when I first heard about tail-call optimization, and honestly, I couldn't believe every language doesn't have it.</p>
<p>Tail-call optimization (TCO) is one of those features that, once you understand it, makes you wonder why it's not everywhere. Some languages have had it for decades. Others, like Java, have been debating it for just as long. Let me break down what it is, how it works, and where you can actually use it.</p>
<h2 id="what-is-tail-call-optimization-and-how-does-it-work">What is tail-call optimization and how does it work?</h2>
<p>Tail-call optimization is a compiler technique that transforms recursive functions so they reuse the current stack frame instead of creating new ones. The result? You can recurse to any depth without ever running out of stack space.</p>
<p>To understand why this matters, you need to understand what happens when a function calls itself.</p>
<h3 id="the-stack-frame-problem">The stack frame problem</h3>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/tail-call-optimization/stack-frame-growth.webp" alt="Stack frames growing with each recursive call until overflow" width="1024" height="1024"></p>
<p>Every time you call a function, the runtime pushes a new frame onto the call stack. That frame holds the function's local variables, parameters, and return address. When the function returns, its frame gets popped off.</p>
<p>With recursion, each recursive call adds another frame. If you're calculating <code>factorial(10000)</code>, that's 10,000 stack frames sitting in memory at once. Most runtimes allocate between 512KB and 1MB for the stack. Go deep enough, and you're done.</p>
<p>Here's a classic factorial in Clojure without TCO:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="clojure" data-theme="material-theme github-light"><code data-language="clojure" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">defn</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">zero?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">    1N</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">*</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">dec</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)))))</span></span></code></pre></figure>
<p>Call <code>(factorial 6)</code> and watch the stack grow:</p>
<pre><code>(factorial 6)
  (* 6 (factorial 5))
    (* 6 (* 5 (factorial 4)))
      (* 6 (* 5 (* 4 (factorial 3))))
        (* 6 (* 5 (* 4 (* 3 (factorial 2)))))
          (* 6 (* 5 (* 4 (* 3 (* 2 (factorial 1))))))
            (* 6 (* 5 (* 4 (* 3 (* 2 (* 1 (factorial 0)))))))
</code></pre>
<p>That's 7 frames stacked up before a single multiplication happens. Each frame has to wait for the one below it to return. For <code>factorial(100000)</code>, you'd need 100,000 frames. Your stack won't survive that.</p>
<h3 id="how-tco-fixes-this">How TCO fixes this</h3>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/tail-call-optimization/tco-frame-reuse.webp" alt="TCO reusing a single stack frame with a recycling loop" width="1024" height="1024"></p>
<p>The trick is restructuring the recursion so the recursive call is the <em>last</em> thing the function does. No multiplication after it. No further computation. Just the recursive call, and nothing else.</p>
<p>Here's the tail-recursive version in Clojure:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="clojure" data-theme="material-theme github-light"><code data-language="clojure" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">defn</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#6F42C1"> factorial</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  ([</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">factorial</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1N</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  ([</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">zero?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      acc</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">recur</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">dec</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">*</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)))))</span></span></code></pre></figure>
<p>The key difference: instead of waiting for the recursive call to return and then multiplying, we pass the running total as an accumulator parameter. The multiplication happens <em>before</em> the recursive call, not after.</p>
<p>Now the stack looks completely different:</p>
<pre><code>(factorial 6 1)
(factorial 5 6)        ;; reused frame
(factorial 4 30)       ;; reused frame
(factorial 3 120)      ;; reused frame
(factorial 2 360)      ;; reused frame
(factorial 1 720)      ;; reused frame
(factorial 0 720)      ;; reused frame
=> 720
</code></pre>
<p>One frame. Reused every time. The compiler sees that nothing happens after the recursive call, so it just jumps back to the top of the function with new arguments. It's basically a <code>while</code> loop under the hood.</p>
<h2 id="which-languages-actually-support-tail-call-optimization">Which languages actually support tail-call optimization?</h2>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/tail-call-optimization/language-support.webp" alt="Language support spectrum — automatic, opt-in, and no TCO" width="1024" height="1024"></p>
<p>Not all languages treat TCO the same way. Some do it automatically, some give you tools to opt in, and some just... don't.</p>
<h3 id="languages-with-automatic-tco">Languages with automatic TCO</h3>
<p>These languages optimize tail calls without you having to ask:</p>
<p><strong>Scheme and Racket</strong> were built around TCO. The Scheme specification (R5RS and later) actually <em>requires</em> implementations to support proper tail calls. It's not optional. This is why Scheme is the go-to language for teaching recursion in computer science programs.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="scheme" data-theme="material-theme github-light"><code data-language="scheme" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">define</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">factorial</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#24292E;--shiki-light-font-style:inherit"> n acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  (</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      acc</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">factorial </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">factorial </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1000000</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> ;; works fine, no stack overflow</span></span></code></pre></figure>
<p><strong>Erlang and Elixir</strong> also do TCO automatically. In the BEAM VM (which powers both), tail-recursive functions are the standard way to write loops. There are no <code>for</code> or <code>while</code> loops in Elixir. Everything is recursion, and TCO makes it work.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="elixir" data-theme="material-theme github-light"><code data-language="elixir" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">defmodule</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Math</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> do</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> do:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> do:</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">end</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">Math</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1_000_000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> # runs in constant stack space</span></span></code></pre></figure>
<p><strong>Haskell</strong> supports TCO, but it gets tricky because of lazy evaluation. A tail call in Haskell might build up unevaluated thunks (delayed computations) instead of actual values. You sometimes need to force strict evaluation with <code>seq</code> or bang patterns to get the full benefit.</p>
<p><strong>OCaml</strong> handles TCO cleanly. The compiler turns tail-recursive functions into loops automatically. OCaml's pattern matching and functional style make tail recursion feel natural.</p>
<h3 id="languages-with-opt-in-tco">Languages with opt-in TCO</h3>
<p><strong>Scala</strong> gives you the <code>@tailrec</code> annotation. It doesn't <em>enable</em> TCO. It tells the compiler to verify that your function is actually tail-recursive and throw a compile error if it's not. The Scala compiler already optimizes tail calls when it can, but <code>@tailrec</code> gives you a guarantee.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="scala" data-theme="material-theme github-light"><code data-language="scala" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> scala</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">annotation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">tailrec</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">@</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">tailrec</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">n</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">BigInt</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">acc</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">: </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">BigInt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> BigInt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) acc</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  else</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factorial(n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">factorial(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100000</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// works perfectly</span></span></code></pre></figure>
<p>If you accidentally write non-tail-recursive code with <code>@tailrec</code>, the compiler tells you:</p>
<pre><code>error: could not optimize @tailrec annotated method factorial:
it contains a recursive call not in tail position
</code></pre>
<p>That's really helpful. You catch the problem at compile time instead of discovering it in production when your app crashes.</p>
<p><strong>Clojure</strong> takes a different approach with <code>recur</code>. The JVM doesn't support TCO natively, so Clojure can't just optimize tail calls transparently. Instead, <code>recur</code> explicitly tells the compiler "jump back to the top of this function." If you try to use <code>recur</code> in a non-tail position, you get a compile error.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="clojure" data-theme="material-theme github-light"><code data-language="clojure" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">defn</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">loop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i n acc </span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1N</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">zero?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      acc</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">recur</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">dec</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">*</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)))))</span></span></code></pre></figure>
<p>Rich Hickey (Clojure's creator) made this a conscious design choice. He wanted developers to <em>know</em> when they're relying on tail-call behavior rather than assuming the compiler will handle it.</p>
<h3 id="languages-that-dont-support-tco">Languages that don't support TCO</h3>
<p><strong>Java</strong> is the big one. TCO has been discussed in the Java community for over 20 years. There was even a <a href="https://mail.openjdk.org/pipermail/loom-dev/">Project Loom mailing list discussion</a> about it. But it keeps getting deprioritized.</p>
<p>Why? A few reasons:</p>
<ol>
<li>
<p><strong>Stack traces are sacred in Java.</strong> TCO eliminates intermediate stack frames. That means your stack trace in an exception won't show every call. For a language where developers rely heavily on stack traces for debugging, that's a real problem.</p>
</li>
<li>
<p><strong>Security model depends on stack inspection.</strong> Java's <code>SecurityManager</code> (deprecated but still influential) walks the call stack to check permissions. TCO would break that.</p>
</li>
<li>
<p><strong>The JVM wasn't designed for it.</strong> Adding TCO to the JVM would require changes to the bytecode verifier and the runtime. It's not impossible, but it's a significant engineering effort for a feature that has workarounds.</p>
</li>
<li>
<p><strong>Iterative style is idiomatic.</strong> Java developers already use <code>for</code> and <code>while</code> loops. The demand for TCO is lower than in functional languages where recursion is the primary looping mechanism.</p>
</li>
</ol>
<p><strong>JavaScript</strong> is an interesting case. The ES2015 (ES6) spec includes proper tail calls. It's in the specification. But only Safari/WebKit actually implements it. V8 (Chrome, Node.js) and SpiderMonkey (Firefox) both implemented it experimentally and then removed it. The V8 team argued it made debugging harder and proposed an alternative called "syntactic tail calls" that would require an explicit keyword. That proposal stalled, and here we are.</p>
<p><strong>Python</strong> doesn't support TCO either. Guido van Rossum <a href="https://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html">wrote a blog post</a> explaining why: he believes stack traces are more valuable than tail-call optimization, and Python's default recursion limit of 1,000 is intentional.</p>
<h2 id="how-can-you-work-around-the-lack-of-tco">How can you work around the lack of TCO?</h2>
<p>If you're stuck in a language without TCO, you have options.</p>
<h3 id="convert-to-iteration">Convert to iteration</h3>
<p>The simplest approach. Any tail-recursive function can be mechanically converted to a loop:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> BigInteger</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    BigInteger</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> BigInteger</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ONE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    for</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">--</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        acc </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">multiply</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BigInteger</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">valueOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>It's not as elegant as recursion, but it works and it's what most Java developers do.</p>
<h3 id="the-trampoline-pattern">The trampoline pattern</h3>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/tail-call-optimization/trampoline-pattern.webp" alt="Trampoline pattern — bounce loop replacing stack recursion with heap allocation" width="1024" height="1024"></p>
<p>A trampoline is a loop that repeatedly calls a function until it produces a result instead of another function call. It simulates TCO by moving the recursion from the call stack to the heap.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">FunctionalInterface</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> bounce</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> isDone</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    default</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> T</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UnsupportedOperationException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> done</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">T</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> bounce</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> isDone</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> T</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        };</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> T</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        while</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isDone</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            trampoline </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">bounce</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BigInteger</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> BigInteger</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">done</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> acc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">multiply</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BigInteger</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">valueOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Usage</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">BigInteger</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Trampoline</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">factorial</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> BigInteger</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ONE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span></code></pre></figure>
<p>The trampoline works because each step returns a lambda (stored on the heap) instead of making a recursive call (stored on the stack). The heap is much bigger than the stack, so you can handle very deep recursion.</p>
<p>It's clever, but it adds overhead from all the lambda allocations and the indirection. For most Java code, just use a loop.</p>
<h2 id="when-does-tail-call-optimization-actually-matter-in-practice">When does tail-call optimization actually matter in practice?</h2>
<p>TCO isn't just an academic exercise. There are real scenarios where it makes a difference.</p>
<h3 id="tree-and-graph-traversal">Tree and graph traversal</h3>
<p>Walking deeply nested data structures (think ASTs, file system trees, or deeply nested JSON) can blow the stack without TCO. Functional languages handle this naturally with tail-recursive traversals using continuation-passing style.</p>
<h3 id="state-machines-and-protocol-handlers">State machines and protocol handlers</h3>
<p>In Erlang/Elixir, processes are often implemented as tail-recursive functions that handle messages in a loop. The <code>GenServer</code> behavior in Elixir is built on this pattern. Each state transition is a tail call, and the process runs forever without growing the stack.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="elixir" data-theme="material-theme github-light"><code data-language="elixir" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">defmodule</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Counter</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> do</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  def</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> loop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">count</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> do</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    receive</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> do</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">      :increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> loop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">count </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">:get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> caller</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        send</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">caller</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> count</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        loop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">count</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    end</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  end</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">end</span></span></code></pre></figure>
<h3 id="compiler-and-interpreter-implementations">Compiler and interpreter implementations</h3>
<p>If you're writing a programming language that supports recursion (and most do), your interpreter needs to handle deep recursion without itself overflowing. TCO in the host language makes this straightforward.</p>
<h3 id="stream-processing">Stream processing</h3>
<p>Processing large sequences element by element (like a log file with millions of lines) maps naturally to recursive patterns. TCO means you can write these as elegant recursive functions instead of imperative loops.</p>
<h2 id="whats-the-real-takeaway-here">What's the real takeaway here?</h2>
<p>Tail-call optimization is one of those features that separates languages designed for recursion from languages that merely tolerate it. Scheme, Elixir, and Haskell treat recursion as a first-class control flow mechanism. Java, Python, and (in practice) JavaScript treat it as a nice-to-have that shouldn't come at the expense of stack traces and debugging.</p>
<p>I don't think Java will ever get TCO. And honestly, with Virtual Threads and the continued push toward imperative patterns in Java, the demand just isn't there. But if you ever work in Scala, Clojure, or Elixir, understanding TCO isn't optional. It's how you write loops.</p>
<p>The next time you hit a <code>StackOverflowError</code>, don't just bump the stack size with <code>-Xss</code>. Ask yourself: can I restructure this as a tail call? And if your language doesn't support TCO, at least you know why, and what your options are.</p>
<p>For more on tail-call optimization, see the <a href="https://r7rs.org/">Scheme R7RS specification</a> which mandates proper tail calls, Guido van Rossum's post on <a href="https://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html">why Python doesn't support TCO</a>, and the <a href="https://docs.scala-lang.org/scala3/book/fp-functions-are-values.html">Scala @tailrec documentation</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Mastering Virtual Threads in Java 25</a> — If Java won't give us TCO, at least it gave us Virtual Threads for concurrency.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-i-turned-daily-problem-solving-into-a-dsa-habit">How I Turned Daily Problem Solving into a DSA Habit</a> — Recursion practice is a big part of building strong DSA fundamentals.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-interview-2025">Java Interview Questions 2025</a> — TCO and recursion concepts come up in advanced Java interviews.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[The Axios npm Hack: How North Korea Hijacked 100M Weekly Downloads]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/axios-npm-supply-chain-attack-2026</link>
      <guid>https://www.rabinarayanpatra.com/blogs/axios-npm-supply-chain-attack-2026</guid>
      <pubDate>Wed, 01 Apr 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[On March 31, 2026, Axios was hijacked on npm by North Korean hackers. Here is the full technical breakdown, how to check if you're affected, and how to protect your projects.]]></description>
      <content:encoded><![CDATA[<p>At 00:21 UTC on March 31, 2026, someone published <code>axios@1.14.1</code> to npm. Within seconds, machines across the globe started phoning home to a server in Panama.</p>
<p>The Axios HTTP client has roughly 100 million weekly downloads. It sits in the dependency tree of almost every Node.js project you've ever touched. And for about 3 hours, anyone who ran <code>npm install</code> got a North Korean Remote Access Trojan installed on their machine.</p>
<p>I spent the past two days pulling apart the malicious payload, reading every post-mortem I could find, and checking my own projects. Here is everything I know.</p>
<h2 id="what-exactly-happened-to-the-axios-npm-package">What exactly happened to the Axios npm package?</h2>
<p>On March 31, 2026, a North Korean state-sponsored hacking group compromised the npm credentials of <strong>@jasonsaayman</strong>, the lead maintainer of Axios. They used those credentials to publish two malicious versions:</p>
<ul>
<li><strong>axios@1.14.1</strong> (tagged <code>latest</code>) at 00:21 UTC</li>
<li><strong>axios@0.30.4</strong> (tagged <code>legacy</code>) at 01:00 UTC</li>
</ul>
<p>Both versions contained a hidden dependency called <code>plain-crypto-js</code> that installed a cross-platform RAT (Remote Access Trojan) through npm's <code>postinstall</code> hook. The RAT connected back to the attacker's command-and-control server within 1.1 seconds of <code>npm install</code> finishing.</p>
<p>Here is the full timeline:</p>
<table>
<thead>
<tr>
<th>Time (UTC)</th>
<th>What Happened</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mar 30, 05:57</td>
<td>Attacker publishes <code>plain-crypto-js@4.2.0</code> (clean decoy to establish registry history)</td>
</tr>
<tr>
<td>Mar 30, 16:03</td>
<td>C2 domain <code>sfrclak.com</code> registered via Namecheap</td>
</tr>
<tr>
<td>Mar 30, 23:59</td>
<td><code>plain-crypto-js@4.2.1</code> published (the actual malicious version)</td>
</tr>
<tr>
<td>Mar 31, 00:05</td>
<td>Automated scanners flag <code>plain-crypto-js@4.2.1</code> as malware (6 minutes after publish)</td>
</tr>
<tr>
<td>Mar 31, 00:21</td>
<td><code>axios@1.14.1</code> published via compromised account</td>
</tr>
<tr>
<td>Mar 31, 01:00</td>
<td><code>axios@0.30.4</code> published (targeting legacy users)</td>
</tr>
<tr>
<td>Mar 31, 01:50</td>
<td>Elastic Security Labs files GitHub Security Advisory</td>
</tr>
<tr>
<td>Mar 31, ~03:15</td>
<td>npm unpublishes both malicious versions</td>
</tr>
</tbody>
</table>
<p><strong>Total exposure window: about 2 hours and 54 minutes.</strong> Given that Axios gets pulled millions of times a day, that's a lot of infected machines.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/axios-attack-timeline.webp" alt="Axios npm attack timeline" width="1024" height="1024"></p>
<h2 id="how-did-the-attacker-compromise-the-axios-maintainers-account">How did the attacker compromise the Axios maintainer's account?</h2>
<p>The attacker got hold of <strong>@jasonsaayman's</strong> npm credentials. We don't know the exact method yet (phishing, credential stuffing, or a reused password), but there were two dead giveaways that this wasn't legitimate:</p>
<p><strong>1. The publishing email changed.</strong> The legitimate maintainer published using <code>jasonsaayman@gmail.com</code>. The malicious publish came from <code>ifstap@proton.me</code>.</p>
<p><strong>2. The provenance chain broke.</strong> Here's the critical detail. Legitimate <code>axios@1.14.0</code> was published using <strong>GitHub Actions OIDC with SLSA provenance attestations</strong>. This means npm could cryptographically verify that the package was built from the actual Axios repo. The malicious <code>1.14.1</code>? Published directly from the CLI with <strong>zero provenance</strong>. That's like a bank wire going through without a signature.</p>
<p>This is the strongest argument for OIDC trusted publishing I've ever seen. If npm had enforced provenance checks, this attack would have been stopped cold.</p>
<h2 id="how-did-the-malicious-code-actually-work">How did the malicious code actually work?</h2>
<p>This is where it gets technically interesting. The attacker didn't modify a single line of Axios source code. Instead, they used what I'm calling a <strong>"phantom dependency"</strong> technique.</p>
<h3 id="the-injection">The injection</h3>
<p>They added one line to <code>package.json</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">dependencies</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">plain-crypto-js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">^4.2.1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This package is <strong>never imported anywhere in the Axios codebase</strong>. It exists only to trigger npm's <code>postinstall</code> hook. When you run <code>npm install</code>, npm automatically executes any <code>postinstall</code> script defined in a dependency's <code>package.json</code>. The <code>plain-crypto-js</code> package had this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">scripts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">postinstall</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">node setup.js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That's it. One line in <code>dependencies</code>, one <code>postinstall</code> script, and every machine that installed Axios became compromised.</p>
<h3 id="the-dropper-setupjs">The dropper (setup.js)</h3>
<p>The <code>setup.js</code> file used two layers of obfuscation:</p>
<ul>
<li><strong>Layer 1</strong>: Reversed Base64 encoding with padding character substitution</li>
<li><strong>Layer 2</strong>: XOR cipher using the key <code>OrDeR_7077</code> with a position-dependent index calculation (<code>7 * i^2 % 10</code>)</li>
</ul>
<p>All the important strings (URLs, commands, module names) were stored in an encoded array and decoded at runtime. The script checked <code>os.platform()</code> to determine which OS it was running on, then downloaded a platform-specific second-stage payload from the C2 server.</p>
<p>And here's the clever part. After execution, <code>setup.js</code> <strong>deleted itself</strong> using <code>fs.unlink(__filename)</code> and swapped the malicious <code>package.json</code> with a clean version. If you looked at <code>node_modules/plain-crypto-js/</code> after the fact, you'd see nothing suspicious. Only your lockfile would tell the truth.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/axios-attack-chain.webp" alt="Technical diagram of the attack chain flow" width="1024" height="1024"></p>
<h3 id="the-rat-remote-access-trojan">The RAT (Remote Access Trojan)</h3>
<p>Three separate implementations, one per OS, all speaking the same C2 protocol:</p>
<p><strong>macOS</strong>: An AppleScript downloaded a C++ binary via <code>curl</code> to <code>/Library/Caches/com.apple.act.mond</code> (disguised as an Apple system daemon).</p>
<p><strong>Windows</strong>: A four-stage chain. VBScript downloads a <code>.ps1</code> file. PowerShell gets copied to <code>%PROGRAMDATA%\wt.exe</code> (masquerading as Windows Terminal) and runs with <code>-NoProfile -ep Bypass</code>. A registry Run key gives it <strong>reboot persistence</strong>.</p>
<p><strong>Linux</strong>: Direct <code>curl</code> download to <code>/tmp/ld.py</code>, executed detached via <code>nohup python3</code>.</p>
<p>All three beaconed home every 60 seconds using HTTP POST with Base64-encoded JSON. The User-Agent string was hardcoded as <code>mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)</code>, which is an Internet Explorer 8 string from the Windows XP era. Spotting that on a macOS or Linux box in 2026 is... not subtle.</p>
<p>The RAT supported four commands:</p>
<table>
<thead>
<tr>
<th>Command</th>
<th>What It Does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>kill</code></td>
<td>Self-terminate (but Windows persistence survives unless cleaned)</td>
</tr>
<tr>
<td><code>runscript</code></td>
<td>Execute arbitrary code (PowerShell, AppleScript, or shell)</td>
</tr>
<tr>
<td><code>peinject</code></td>
<td>Reflective .NET assembly loading on Windows, binary drop on macOS/Linux</td>
</tr>
<tr>
<td><code>rundir</code></td>
<td>Filesystem enumeration (names, sizes, timestamps, child directories)</td>
</tr>
</tbody>
</table>
<p>On first beacon, the RAT sent back everything: hostname, username, OS version, timezone, boot time, hardware model, CPU type, and a full process list (up to 1,000 entries on macOS). The attacker had a complete picture of your machine within seconds.</p>
<h2 id="who-was-behind-the-axios-npm-attack">Who was behind the Axios npm attack?</h2>
<p>Both Microsoft and Google published independent attributions within 24 hours.</p>
<p><strong>Microsoft</strong> attributed the attack to <strong>Sapphire Sleet</strong>, a North Korean state-sponsored group also tracked as BlueNoroff, STARDUST CHOLLIMA, and CryptoCore.</p>
<p><strong>Google's Threat Intelligence Group (GTIG)</strong> attributed it to <strong>UNC1069</strong>, a financially motivated North Korean threat actor active since at least 2018. The macOS binary showed "significant overlap" with known samples of <strong>WAVESHAPER.V2</strong>, a backdoor previously used by this group.</p>
<p><strong>SANS</strong> noted a possible connection to the <strong>TeamPCP</strong> campaign. Between March 19-27, 2026, TeamPCP compromised several other tools: the Trivy scanner, the KICS scanner, LiteLLM on PyPI, and Telnyx on PyPI. The evidence suggests TeamPCP may be an Initial Access Broker sitting on a stockpile of stolen publishing credentials.</p>
<p>This wasn't some random script kiddie. This was a state-sponsored operation with preparation, patience, and cross-platform engineering.</p>
<h2 id="how-do-you-check-if-your-projects-are-affected">How do you check if your projects are affected?</h2>
<p>If any of your projects ran <code>npm install</code>, <code>npm update</code>, or <code>yarn install</code> (without a committed lockfile) between 00:21 and 03:15 UTC on March 31, 2026, you need to check.</p>
<h3 id="step-1-search-your-lockfiles">Step 1: Search your lockfiles</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># npm</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"axios"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> package-lock.json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1\.14\.1|0\.30\.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># yarn</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">axios@</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> yarn.lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1\.14\.1|0\.30\.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># pnpm</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">axios</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> pnpm-lock.yaml</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> grep</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -E</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1\.14\.1|0\.30\.4</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span></code></pre></figure>
<h3 id="step-2-check-for-the-phantom-dependency">Step 2: Check for the phantom dependency</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> plain-crypto-js</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">find</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> node_modules</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">plain-crypto-js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -type</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> d</span></span></code></pre></figure>
<h3 id="step-3-search-across-all-your-local-projects">Step 3: Search across ALL your local projects</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">find</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ~</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -type</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> d</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">plain-crypto-js</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">*/node_modules/*</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span></span></code></pre></figure>
<h3 id="step-4-check-for-rat-artifacts-on-your-machine">Step 4: Check for RAT artifacts on your machine</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># macOS</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -la</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /Library/Caches/com.apple.act.mond</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x26;&#x26;</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> echo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">INFECTED</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Linux</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ls</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -la</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> /tmp/ld.py</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> 2></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/dev/null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x26;&#x26;</span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5"> echo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">INFECTED</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span></code></pre></figure>
<p>On Windows, check for <code>%PROGRAMDATA%\wt.exe</code> and <code>%TEMP%\6202033.vbs</code>.</p>
<h3 id="step-5-check-network-logs-for-c2-traffic">Step 5: Check network logs for C2 traffic</h3>
<p>Look for outbound connections to <code>sfrclak.com</code> or <code>142.11.206.73</code> on port 8000. Also search for the distinctive User-Agent string: <code>mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)</code>.</p>
<p><strong>If you find any of these, assume full compromise.</strong> That means rotating every credential, API key, SSH key, and token that was accessible from that machine. I'm not exaggerating. The RAT could execute arbitrary code within seconds of connecting.</p>
<h2 id="what-should-you-do-right-now-to-protect-your-projects">What should you do right now to protect your projects?</h2>
<p>Even if you weren't hit, this is a wake-up call. Here's what I've done across all my projects this week.</p>
<h3 id="lock-your-dependencies">Lock your dependencies</h3>
<p>If you're not committing lockfiles (<code>package-lock.json</code>, <code>yarn.lock</code>, <code>pnpm-lock.yaml</code>), start today. A committed lockfile from before March 31 would have <strong>completely prevented</strong> this attack because <code>npm ci</code> refuses to install versions not in the lockfile.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># In CI/CD, ALWAYS use npm ci instead of npm install</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ci</span></span></code></pre></figure>
<p><code>npm ci</code> is not just faster. It's a security boundary. It fails if the lockfile doesn't match <code>package.json</code> and never rewrites the lockfile. This means a compromised upstream version won't sneak in.</p>
<h3 id="enable-min-release-age">Enable min-release-age</h3>
<p>This is a new feature that delays automatic adoption of freshly published package versions. If this had been enabled, the malicious Axios versions would never have been installed because they were less than 3 hours old.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ini" data-theme="material-theme github-light"><code data-language="ini" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># .npmrc</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">min-release-age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">7d</span></span></code></pre></figure>
<p>This is now supported on npm (v11.10.0+), pnpm (v10.16+), Yarn (v4.10+), and Bun (v1.3+). I've set it to 7 days on all my projects. The tradeoff is that you wait a week for new versions, but honestly, how often do you need a dependency update within hours of release?</p>
<h3 id="kill-postinstall-scripts">Kill postinstall scripts</h3>
<p>The entire attack relied on npm automatically running <code>postinstall</code> scripts. You can disable this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="ini" data-theme="material-theme github-light"><code data-language="ini" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># .npmrc</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">ignore-scripts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">true</span></span></code></pre></figure>
<p>Or per-install:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ci</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> --ignore-scripts</span></span></code></pre></figure>
<p>The downside is that some legitimate packages need postinstall scripts (like <code>sharp</code> for image processing or <code>esbuild</code> for native binaries). You'll need to run those manually or allowlist them. But for most projects, the security benefit outweighs the convenience.</p>
<h3 id="add-dependency-scanning-to-your-ci">Add dependency scanning to your CI</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># GitHub Actions example</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">-</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Audit dependencies</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npm audit --audit-level=high</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">-</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> Check for known malicious packages</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> npx socket-security/cli scan</span></span></code></pre></figure>
<p>Tools like <a href="https://socket.dev">Socket.dev</a>, <a href="https://snyk.io">Snyk</a>, and <a href="https://www.stepsecurity.io">StepSecurity Harden-Runner</a> can catch anomalous behavior (like unexpected network connections during <code>npm install</code>) before it reaches production.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/axios-npm-security-checklist.webp" alt="Developer security checklist to prevent supply chain attacks" width="1024" height="1024"></p>
<h2 id="how-does-this-compare-to-past-npm-supply-chain-attacks">How does this compare to past npm supply chain attacks?</h2>
<p>Supply chain attacks on npm aren't new. But the scale keeps getting worse.</p>
<table>
<thead>
<tr>
<th>Incident</th>
<th>Year</th>
<th>Weekly Downloads</th>
<th>Attack Vector</th>
<th>Payload</th>
</tr>
</thead>
<tbody>
<tr>
<td>event-stream</td>
<td>2018</td>
<td>1.5M</td>
<td>Social engineering (maintainer gave away access)</td>
<td>Cryptocurrency stealer</td>
</tr>
<tr>
<td>ua-parser-js</td>
<td>2021</td>
<td>7M</td>
<td>Compromised credentials</td>
<td>Crypto miner + password stealer</td>
</tr>
<tr>
<td>colors.js / faker.js</td>
<td>2022</td>
<td>20M</td>
<td>Maintainer self-sabotage (protest)</td>
<td>Infinite loop / data wipe</td>
</tr>
<tr>
<td><strong>Axios</strong></td>
<td><strong>2026</strong></td>
<td><strong>100M</strong></td>
<td><strong>Compromised credentials</strong></td>
<td><strong>Cross-platform state-sponsored RAT</strong></td>
</tr>
</tbody>
</table>
<p>The jump from 20M to 100M weekly downloads is scary enough. But the real difference is who's behind it. The event-stream attack was one developer targeting one cryptocurrency wallet. The Axios attack was a nation-state operation with custom RATs for three operating systems, anti-forensic self-deletion, and a coordinated campaign that also hit Trivy, KICS, and several PyPI packages the same month.</p>
<p>We've crossed a line. Supply chain attacks are no longer a "low probability, low impact" risk. They are an active, ongoing threat from well-resourced adversaries.</p>
<h2 id="why-does-npms-architecture-make-supply-chain-attacks-so-dangerous">Why does npm's architecture make supply chain attacks so dangerous?</h2>
<p>I want to be honest about something. The npm ecosystem has a structural problem that makes attacks like this possible. And it's not just about bad password hygiene.</p>
<p><strong>Transitive trust is the real issue.</strong> When you run <code>npm install axios</code>, you're not just trusting the Axios maintainers. You're trusting every dependency of Axios, and every dependency of those dependencies. A single compromised account anywhere in that tree can inject code into your build. The Axios attack exploited this by adding <code>plain-crypto-js</code> as a dependency. Most developers would never notice a new sub-dependency appearing.</p>
<p><strong>Postinstall scripts run with full system access.</strong> When npm executes a <code>postinstall</code> hook, it runs with whatever permissions your user account has. There's no sandboxing, no permission prompt, no "this package wants to access your filesystem" dialog. It just runs.</p>
<p><strong>The window between publish and detection is all the attacker needs.</strong> Automated scanners flagged <code>plain-crypto-js</code> within 6 minutes. But the malicious Axios version wasn't pulled for nearly 3 hours. For a package with 100M weekly downloads, that's plenty of time.</p>
<p>Compare this to other ecosystems. Go modules use checksums verified against a transparency log. Rust's crates.io doesn't support postinstall scripts. Python's PyPI is adding Trusted Publishers. npm has started moving in this direction with OIDC and provenance, but adoption is still optional.</p>
<h2 id="what-is-the-one-thing-i-learned-from-this-incident">What is the one thing I learned from this incident?</h2>
<p>The Axios team was actually doing a lot of things right. They had moved to OIDC trusted publishing. They had GitHub Actions automating releases. The legitimate 1.14.0 had full provenance attestations.</p>
<p>But none of that mattered because npm still allowed a direct CLI publish to override everything. The attacker bypassed the entire CI/CD pipeline by publishing from a stolen token.</p>
<p>The fix isn't just "use better passwords" or "enable 2FA" (though you should absolutely do both). The fix is <strong>making provenance-verified publishing mandatory</strong>. If npm had rejected the 1.14.1 publish because it lacked OIDC provenance, this attack would have failed. Period.</p>
<p>I think we're heading there. npm has the infrastructure. The question is whether they'll make it a requirement before the next attack, or after.</p>
<p>In the meantime, I've added <code>min-release-age=7d</code> and <code>ignore-scripts=true</code> to every <code>.npmrc</code> in every project I maintain. I've verified my lockfiles are committed. And I've added Socket.dev to my CI pipelines. It's not foolproof, but it's a lot better than trusting that the 1,500 packages in my dependency tree all have uncompromised maintainer accounts.</p>
<p>Stay safe out there.</p>
<p>For the official post-mortems and technical analysis, see:</p>
<ul>
<li><a href="https://www.microsoft.com/en-us/security/blog/2026/04/01/mitigating-the-axios-npm-supply-chain-compromise/">Microsoft Security Blog on the Axios Compromise</a></li>
<li><a href="https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package">Google GTIG's Attribution Report</a></li>
<li><a href="https://www.elastic.co/security-labs/axios-one-rat-to-rule-them-all">Elastic Security Labs' Detailed Analysis</a></li>
<li><a href="https://www.sans.org/blog/axios-npm-supply-chain-compromise-malicious-packages-remote-access-trojan">SANS Institute's Breakdown</a></li>
<li><a href="https://github.com/advisories/GHSA-fw8c-xr5c-95f9">GitHub Security Advisory GHSA-fw8c-xr5c-95f9</a></li>
<li><a href="https://snyk.io/blog/axios-npm-package-compromised-supply-chain-attack-delivers-cross-platform/">Snyk's Supply Chain Attack Analysis</a></li>
<li><a href="https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan">StepSecurity's First Detection Report</a></li>
<li><a href="https://www.wiz.io/blog/axios-npm-compromised-in-supply-chain-attack">Wiz's Impact Assessment</a></li>
<li><a href="https://techcrunch.com/2026/03/31/hacker-hijacks-axios-open-source-project-used-by-millions-to-push-malware/">TechCrunch Coverage</a></li>
<li><a href="https://github.com/axios/axios/issues/10604">GitHub Issue #10604 (Original Community Report)</a></li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet">The Day a React Patch Broke the Internet</a> — Another case where a security response caused more damage than the original threat. Same energy, different failure mode.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-Driven Anomaly Detection with Spring AI</a> — If you're thinking about detecting compromised dependencies at runtime, this post covers behavioral analysis patterns.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero Trust Architecture in Spring Boot Microservices</a> — Supply chain attacks are exactly why "trust nothing, verify everything" matters at every layer.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[You Can Now Pay by Just Saying It. Here Is What Razorpay and NPCI Just Pulled Off]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/agentic-payments-razorpay-npci-upi</link>
      <guid>https://www.rabinarayanpatra.com/blogs/agentic-payments-razorpay-npci-upi</guid>
      <pubDate>Thu, 26 Feb 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Razorpay and NPCI just launched Agentic Payments on Claude, letting AI complete UPI transactions on your behalf. Here is a breakdown of how it works, why India is the right place for it, and what it means for the future of commerce.]]></description>
      <content:encoded><![CDATA[<p>I was watching the India AI Impact Summit coverage and came across the Razorpay and NPCI announcement. On the surface it sounded like another "AI + payments" press release. Then I actually read through what they built and it clicked. This is not marketing speak. Something real landed here.</p>
<p>They have made it so you can tell an AI assistant to order your groceries, and it will just do it. Including paying for it. Without you touching a PIN, scanning a QR code, or opening a separate app. That is the short version. The longer version is more interesting.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/agentic-payments-cover.webp" alt="Agentic Payments on Claude for UPI shopping in India" width="640" height="640"></p>
<h2 id="what-agentic-payments-actually-means">What "Agentic Payments" Actually Means</h2>
<p>The word "agentic" has been floating around the AI world for a while now. It refers to AI systems that do not just answer questions but actually take actions on your behalf. Tool use, multi-step planning, executing tasks in the real world.</p>
<p>Payments were always the wall. You could have the smartest AI assistant helping you decide what to buy, compare prices, build a grocery list, but the moment money needed to move, you had to step in. Enter your PIN. Approve an OTP. Confirm a payment. The AI was always the navigator, never the driver.</p>
<p>What Razorpay built, in partnership with NPCI, removes that wall.</p>
<p>The idea is simple: authorize the AI once, set spending limits, and let it handle transactions on your behalf within those guardrails. It is built on top of UPI Reserve Pay, which is essentially a mandate-based UPI framework that already exists and is already regulated.</p>
<p>So this is not some experimental sandbox thing. It is real UPI, with real consumer protections, with a new layer on top that lets an AI agent trigger payments.</p>
<h2 id="how-do-agentic-upi-payments-process-a-transaction">How do agentic UPI payments process a transaction?</h2>
<p>Let me walk through what a typical interaction looks like.</p>
<p>You open Claude and type something like, "Order my usual snacks before the match tonight, keep it under 500 rupees." Claude processes that. It knows from your past orders what "usual snacks" means. It checks Zepto or Swiggy, finds the items, sees they are within budget, and places the order. The payment goes through. You get a confirmation. You never left the chat.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/agentic-payments-flow.webp" alt="Agentic AI payment flow from intent to transaction confirmation" width="640" height="640"></p>
<p>Behind that interaction, here is what is happening technically:</p>
<ol>
<li>The AI interprets intent and maps it to a merchant action.</li>
<li>It checks the user's pre-authorized spending limits for that merchant.</li>
<li>It calls the merchant API to place the order.</li>
<li>UPI Reserve Pay executes the payment using the pre-set mandate, no OTP, no PIN prompt.</li>
<li>The transaction is logged and the user sees a confirmation in the chat.</li>
</ol>
<p>That fourth step is the critical one. UPI Reserve Pay already handles things like recurring subscriptions, auto-debits for utilities, and mandate-based payments. Razorpay and NPCI have applied the same underlying mechanism to this new use case and wired it into an AI experience.</p>
<h2 id="why-is-the-consent-model-critical-for-agentic-payments">Why is the consent model critical for agentic payments?</h2>
<p>This is the part I think is easy to overlook but is actually the whole story.</p>
<p>The reason giving an AI the ability to spend your money sounds scary is because we assume it means handing over the keys completely. That is not what this is.</p>
<p>Here is how consent works in this system: you go in once, you authorize a merchant (say Zomato), and you set a per-transaction spending limit. Maybe 800 rupees. The AI can now complete transactions at Zomato on your behalf, but only up to 800 rupees per transaction, and only for that merchant. You can revoke that consent at any time.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/agentic-payments-upi-reserve.webp" alt="UPI Reserve Pay consent and spending limit authorization screen" width="640" height="640"></p>
<p>That is a really thoughtful model. It gives you the convenience without making you feel like you have signed a blank cheque. And because it is sitting on UPI's existing infrastructure, every transaction is auditable, reversible within limits, and covered by the same consumer protection norms that govern all UPI payments.</p>
<p>You still control the outer ring. The AI operates within it.</p>
<h2 id="why-zomato-swiggy-and-zepto-make-sense-as-the-starting-point">Why Zomato, Swiggy, and Zepto Make Sense as the Starting Point</h2>
<p>This is a smart choice of pilot partners, and not just because they are popular.</p>
<p>These three platforms cover food, grocery, and quick commerce. They are high-frequency, low-consideration purchase categories. When you order lunch or restock protein powder, you are not deliberating for days. The decision window is short. The amounts are predictable. The products are familiar.</p>
<p>This is exactly the kind of commerce where AI friction removal has the highest payoff. You are not asking AI to help you buy a laptop or book a flight. You are asking it to execute a repeating, habitual transaction that you have done dozens of times before.</p>
<p>The AI does not need to be creative here. It needs to be accurate, fast, and invisible. And that is precisely what this setup enables.</p>
<h2 id="why-india-was-the-right-country-for-this-to-launch-in">Why India Was the Right Country for This to Launch In</h2>
<p>This could not have happened anywhere else first, and I mean that technically, not patriotically.</p>
<p>Most countries do not have the infrastructure to do this safely at scale. You need:</p>
<ul>
<li>A real-time payment network with mandate support</li>
<li>Deep digital commerce penetration</li>
<li>An existing base of UPI-trained consumers who are already comfortable with digital payments</li>
<li>Regulatory infrastructure that understands and can accommodate consent-based AI transactions</li>
</ul>
<p>India has all of that. UPI is not just a payments layer, it is a full-stack public infrastructure with features like mandates, recurring payments, and real-time settlement built in. The NPCI already has protocols for automated, pre-authorized debits. Razorpay just needed to build the AI-facing interface on top.</p>
<p>On the consumer side: millions of Indians already tap, scan, or type to pay for groceries and food daily. The behavioral habit is there. The trust in UPI is there. The mobile-first culture is there. Plugging AI into that chain is a much shorter leap than it looks.</p>
<h2 id="how-will-agentic-payments-change-indian-commerce">How will agentic payments change Indian commerce?</h2>
<p>I have been thinking about the phrase "from intent to economic action" that Razorpay used in their announcement. It is a little formal, but it captures something real.</p>
<p>Right now, commerce involves a lot of steps between wanting something and having it. You open an app, you search, you browse, you add to cart, you checkout, you pay, you wait. AI has been slowly eating into some of those steps. Recommendation engines handle search. Auto-fill handles checkout. Saved payment methods reduce friction at payment.</p>
<p>Agentic Payments is the next move in that sequence. The AI does not just assist; it executes. You do not lose control. You just delegate the execution of a clearly bounded task.</p>
<p>The interesting question is where this goes from here. Food and groceries are the easy starting point because the purchases are frequent and small. But the same model, with appropriate consent structures, could extend to bill payments, travel bookings, subscription renewals, or any other category where you have a recurring intent and a predictable amount.</p>
<p>The infrastructure Razorpay and NPCI have built is not category-specific. It is a general framework for AI-driven payments on UPI. The categories they launch with are just the first step.</p>
<h2 id="what-does-this-mean-for-indias-fintech-future">What does this mean for India's fintech future?</h2>
<p>The announcement also signals something about where the serious AI work is happening in India. This was not a hackathon demo. It was a production pilot, announced at a major policy summit, built on top of national payment infrastructure, with real consumer applications from day one.</p>
<p>That says something about how seriously Indian fintech is treating the AI layer. Razorpay has been working steadily on making payments smarter for a while, and this is the most architecturally interesting move they have made yet. NPCI's willingness to build the underlying mandate framework for this use case is equally notable. You do not get that kind of infrastructure cooperation in most markets.</p>
<p>I will be watching how the pilot performs and how quickly it rolls out. If the UX is smooth and the consent model holds up in practice, this has a real shot at becoming normal behavior for a large chunk of Indian smartphone users.</p>
<p>And when that happens, the question "did you pay?" will just mean "did you talk to your AI about it?"</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://www.npci.org.in/what-we-do/upi/product-overview">NPCI Official UPI Product Page</a> — UPI infrastructure and mandate support</li>
<li><a href="https://www.rbi.org.in/Scripts/BS_ViewMasDirections.aspx?id=12024">RBI Master Direction on Digital Payment Security</a> — Regulatory framework for digital payments in India</li>
<li><a href="https://razorpay.com/docs/">Razorpay Developer Documentation</a> — Technical integration details for payment APIs</li>
</ul>
<hr>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">Implementing the Outbox Pattern with CDC in Microservices</a> — The distributed systems reliability patterns that underpin payment infrastructure like this.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/google-antigravity">Google Antigravity: Why I Think It Changes Everything</a> — More thoughts on how agentic AI is reshaping what developers and users can accomplish.</li>
</ul>
<p><em>You can sign up for early access to the Agentic Payments pilot at <a href="https://razorpay.typeform.com/to/x3kpV0RF">Razorpay's signup page</a>. The pilot currently covers Zomato, Swiggy, and Zepto through Claude.</em></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[The Future of Security: AI-Driven Anomaly Detection with Spring AI]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security</link>
      <guid>https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security</guid>
      <pubDate>Sun, 22 Feb 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Move from static rules to dynamic behavioral analysis. Learn how to intercept Spring Security Lifecycle events and feed them into a local LLM or Vector DB to detect account takeover attempts in real-time.]]></description>
      <content:encoded><![CDATA[<p>Static rules ("Block after 3 failed attempts") are necessary, but they are predictable. Sophisticated attackers know these rules. They rotate IPs. They sleep between attempts. They use valid credentials purchased from the dark web (Credential Stuffing).</p>
<p>To catch these, we need <strong>Behavioral Analysis</strong>. We need a system that has "intuition."</p>
<p>In this guide, we will build a "Risk Engine" using <strong>Spring Security Events</strong> and <strong>Spring AI</strong>. It sounds like Science Fiction, but with the tools we have today, it's surprisingly accessible.</p>
<h2 id="what-is-ai-driven-anomaly-detection-in-spring-security">What is AI-driven anomaly detection in Spring Security?</h2>
<ol>
<li><strong>Intercept</strong>: Listen for every login (Success or Failure).</li>
<li><strong>Enrich</strong>: Add context (GeoIP, Device Fingerprint, Time of Day).</li>
<li><strong>Analyze</strong>: Ask an AI model: <em>"Given this user usually logs in from London on a Mac, is this login from a Linux server in Panama suspicious?"</em></li>
<li><strong>React</strong>: If Risk > 80, revoke session or trigger MFA.</li>
</ol>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/risk-engine-flow.webp" alt="Risk Engine Flow" width="1024" height="1024"></p>
<h2 id="how-do-you-capture-spring-security-lifecycle-events-for-anomaly-detection">How do you capture Spring Security lifecycle events for anomaly detection?</h2>
<p>Spring Security publishes events automatically. We just need to catch them.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> AuthEventListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> RiskAnalysisService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> riskService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> AuthEventListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">RiskAnalysisService</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> riskService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">riskService </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> riskService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EventListener</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Async</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Crucial: Don't block the login thread!</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> onSuccess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">AuthenticationSuccessEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Authentication</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAuthentication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        WebAuthenticationDetails</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> details </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">WebAuthenticationDetails</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getDetails</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        LoginContext</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ctx </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> LoginContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            details</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getRemoteAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            LocalDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        riskService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">analyze</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EventListener</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Async</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> onFailure</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">AbstractAuthenticationFailureEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Track brute force patterns</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-does-spring-ai-score-authentication-risk-in-real-time">How does Spring AI score authentication risk in real time?</h2>
<p>This is where the magic happens. We construct a prompt for the LLM.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> RiskAnalysisService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ChatClient</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chatClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserHistoryRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> historyRepo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> RiskAnalysisService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ChatClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Builder</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserHistoryRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">chatClient </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">historyRepo </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> analyze</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">LoginContext</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 1. Fetch historical baseline</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">LoginEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> distinctHistory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> historyRepo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findRecentDistinctLogins</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 2. Construct Prompt</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> prompt </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> """</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            You are a security analyst. </span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            User '%s' is attempting to login from IP %s.</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            </span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            Their recent history includes: %s.</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            </span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            Analyze the probability of account takeover.</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            Return ONLY a JSON object: {"riskScore": 0-100, "reason": "..."}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">        """</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">formatted</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ip</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> distinctHistory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 3. Call AI (Ollama/OpenAI)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chatClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">prompt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">call</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        handleRiskDecision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ctx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-automated-actions-should-trigger-on-suspicious-behavior">What automated actions should trigger on suspicious behavior?</h2>
<p>If the AI returns a high risk score, we can't block the login (it already happened), but we can kill the session immediately.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleRiskDecision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jsonResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    RiskAssessment</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> assessment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> parse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">jsonResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">assessment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">score </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 80</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">HIGH RISK LOGIN DETECTED: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> assessment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">reason</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Revoke all sessions for this user</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        sessionRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAllSessions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SessionInformation</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">expireNow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Trigger Email Alert</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        notificationService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sendSecurityAlert</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> assessment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">reason</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="why-are-vector-databases-essential-for-scaling-anomaly-detection">Why are vector databases essential for scaling anomaly detection?</h2>
<p>Passing raw text history to an LLM context window is expensive. For production, you would use <strong>Vector Search</strong>.</p>
<ol>
<li>Store login metadata embeddings in <strong>pgvector</strong> or <strong>Weaviate</strong>.</li>
<li>When a user logs in, embed the current context.</li>
<li>Query distance to previous logins.</li>
<li>If distance is large (Vector Anomaly), flag it.</li>
</ol>
<p>Spring AI supports <code>VectorStore</code> interfaces exactly for this purpose.</p>
<h2 id="conclusion">Conclusion</h2>
<p>By merging the event-driven architecture of <strong>Spring Security</strong> with the probabilistic reasoning of <strong>Spring AI</strong>, we move from "Rules" to "Intuition".</p>
<p>This allows us to catch novel attacks that static IF-statements would miss. And honestly? It's just really cool to build.</p>
<p>For more information on the technologies discussed, see the <a href="https://docs.spring.io/spring-ai/reference/">Spring AI Reference Documentation</a>, the <a href="https://docs.spring.io/spring-security/reference/servlet/authentication/events.html">Spring Security Events API</a>, and the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Credential_Stuffing_Prevention_Cheat_Sheet.html">OWASP Credential Stuffing Prevention Cheat Sheet</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero Trust Architecture in Spring Boot Microservices</a> — Pair anomaly detection with mTLS and ABAC for defense-in-depth.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution">The Component Revolution: Mastering Spring Security 6.x</a> — The SecurityFilterChain model that powers these event listeners.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/agentic-payments-razorpay-npci-upi">Agentic Payments: Building AI-First UPI with Razorpay</a> — Another take on AI-powered financial systems.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Zero Trust Architecture: Implementing Least Privilege in Spring Boot Microservices]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security</link>
      <guid>https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security</guid>
      <pubDate>Sun, 15 Feb 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Move beyond perimeter security. Learn how to implement mTLS, JWT propagation, and fine-grained Attribute-Based Access Control (ABAC) using custom AuthorizationManagers in Spring Security 6.]]></description>
      <content:encoded><![CDATA[<p>The era of "Castle and Moat" security is over.</p>
<p>We used to think that if we just had a really strong firewall, we were safe. But in a cloud-native world, the bad guys are already inside the castle. Code injection, compromised containers, and side-channel attacks mean we cannot trust a request just because it comes from <code>192.168.x.x</code>.</p>
<p><strong>Zero Trust</strong> means exactly what it says:</p>
<ol>
<li>Verify explicitly.</li>
<li>Use least privilege access.</li>
<li>Assume breach.</li>
</ol>
<p>In this guide, we will implement this in a Spring Boot ecosystem using <strong>mTLS</strong> and <strong>Custom Authorization Managers</strong>.</p>
<h2 id="how-does-mtls-enforce-zero-trust-between-spring-boot-microservices">How does mTLS enforce zero trust between Spring Boot microservices?</h2>
<p>Before we even talk about Users, Services must trust each other.</p>
<p>Standard SSL implies the server has a certificate. <strong>mTLS</strong> implies the client has one too. It's like checking ID at the door, both ways.</p>
<p>In Spring Boot, enabling this is configuration-driven.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  ssl</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    client-auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> need</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> # This enforces mTLS</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    key-store</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> classpath:identity/service-keystore.p12</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    key-store-password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> changeit</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    trust-store</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> classpath:identity/truststore.p12</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    trust-store-password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> changeit</span></span></code></pre></figure>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/mtls-handshake-visual.webp" alt="mTLS Handshake Visual" width="1024" height="1024"></p>
<p>When <code>Service A</code> calls <code>Service B</code>, it must present a valid certificate signed by your internal CA (Certificate Authority). If a rogue container tries to <code>curl</code> your API without the cert, the connection is dropped at the TCP handshake level. Brutal, but effective.</p>
<h2 id="why-is-jwt-propagation-essential-for-zero-trust-service-communication">Why is JWT propagation essential for zero trust service communication?</h2>
<p>Just because <code>Service A</code> is allowed to talk to <code>Service B</code> (mTLS), doesn't mean the <strong>User</strong> initiating the request is allowed to see the data.</p>
<p>We must propagate the user's JWT token across the mesh.</p>
<p>Using <code>Spring Cloud Gateway</code> or manual <code>WebClient</code> filters, we forward the Authorization header.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Propagating the Token</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Mono</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ClientResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> filter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ClientRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ExchangeFunction</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> next</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ReactiveSecurityContextHolder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SecurityContext</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getAuthentication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCredentials</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> instanceof</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AbstractOAuth2Token</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ClientRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">h </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> h</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setBearerAuth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getTokenValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        })</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">flatMap</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">next</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">exchange</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-does-abac-replace-role-based-access-in-zero-trust-architectures">How does ABAC replace role-based access in zero trust architectures?</h2>
<p>Roles (<code>ROLE_ADMIN</code>) are often too broad. Zero Trust demands context.</p>
<p>Existing <code>@PreAuthorize</code> annotations are great, but for complex logic, you want a <strong>Custom Authorization Manager</strong>.</p>
<p>Let's build a policy that allows access only if:</p>
<ol>
<li>The user has the <code>read</code> scope.</li>
<li>The user is in the same <code>region</code> as the resource.</li>
<li>The request comes from a trusted device IP.</li>
</ol>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ContextAwarePolicyManager</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> AuthorizationManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestAuthorizationContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AuthorizationDecision</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> check</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">      Supplier</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Authentication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> authentication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">      RequestAuthorizationContext</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> object</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Authentication</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> authentication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HttpServletRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 1. Check Scope</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> hasScope </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAuthorities</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyMatch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">a </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> a</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAuthority</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">SCOPE_read</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">hasScope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> AuthorizationDecision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 2. Check Custom Attribute (Region)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRegion </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ((</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Jwt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPrincipal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getClaimAsString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">region</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> resourceRegion </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getParameter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">region</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userRegion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> resourceRegion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">             return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> AuthorizationDecision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 3. Risk Engine Check (Mock)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> isSafeIp </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> RiskEngine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">evaluateIp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getRemoteAddr</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> AuthorizationDecision</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">isSafeIp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="applying-the-policy">Applying the Policy</h3>
<p>Now, wire it into your security chain.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> filterChain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> throws Exception </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Apply our custom manager to specific detailed routes</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/v1/sensitive/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">access</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ContextAwarePolicyManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="conclusion">Conclusion</h2>
<p>Zero Trust is not a product you buy; it's a discipline you practice. By combining transport security (mTLS) with identity propagation and rigorous, attribute-based policy enforcement, you build effective defense-in-depth for your microservices.</p>
<p>For further reading, see the <a href="https://csrc.nist.gov/pubs/sp/800/207/final">NIST Special Publication 800-207: Zero Trust Architecture</a>, the <a href="https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html">Spring Security Authorization Architecture</a>, and the <a href="https://docs.spring.io/spring-boot/how-to/webserver.html#howto.webserver.configure-ssl">mTLS with Spring Boot Guide</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution">The Component Revolution: Mastering Spring Security 6.x</a>. The SecurityFilterChain foundation this guide's policies plug into.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">How to Implement the Debezium Outbox Pattern in Spring Boot</a>. The data-integrity counterpart to zero-trust. Same microservices mesh, different failure mode (dual writes instead of stolen tokens).</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs">Full-Stack Social Identity: Spring Security + Next.js + Auth.js</a>. Add OAuth2 and JWT propagation to your zero trust mesh.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-Driven Anomaly Detection with Spring Security</a>. Detect threats in real time using Spring Security events and AI.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Full-Stack Social Identity: Spring Security 6.x + Next.js 16 + Auth.js]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs</link>
      <guid>https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs</guid>
      <pubDate>Sun, 08 Feb 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Building a secure "Backend for Frontend" (BFF) architecture using Next.js 16, Auth.js, and Spring Security 6.4 as a stateless OAuth2 Resource Server.]]></description>
      <content:encoded><![CDATA[<p>Identity is hard. Distributed identity? That's a whole other level of pain.</p>
<p>If you're building a modern full-stack application, you likely have a high-performance frontend (Next.js 16) and a robust, transactional backend (Spring Boot 3.4+).</p>
<p>Connecting them securely is where 90% of developers create vulnerabilities. I've seen it all—tokens in local storage, exposed client secrets, you name it.</p>
<p>In this guide, we’re going to fix that. We’ll implement the <strong>BFF (Backend for Frontend)</strong> pattern. We will use <strong>Auth.js</strong> (formerly NextAuth) to handle the social login flow, and <strong>Spring Security 6.4</strong> to act as a blind, stateless Resource Server that trusts those tokens.</p>
<h2 id="how-does-the-bff-pattern-work-with-spring-security-and-authjs">How does the BFF pattern work with Spring Security and Auth.js?</h2>
<ol>
<li><strong>User</strong> clicks "Login with GitHub" on Next.js.</li>
<li><strong>Next.js (Server Identity)</strong> handles the OAuth2 code grant. It receives an <code>access_token</code> and <code>id_token</code>.</li>
<li><strong>Auth.js</strong> stores this session in an encrypted <code>HttpOnly</code> cookie.</li>
<li><strong>Client Request</strong> goes to Next.js API Route / Server Action.</li>
<li><strong>Next.js</strong> retrieves the <code>access_token</code> from the session and calls Spring Boot.</li>
<li><strong>Spring Boot</strong> validates the JWT signature against GitHub's JWKS (JSON Web Key Set).</li>
</ol>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/bff-pattern-diagram.webp" alt="BFF Pattern Diagram" width="1024" height="1024"></p>
<h2 id="how-do-you-configure-authjs-with-nextjs-16-for-social-login">How do you configure Auth.js with Next.js 16 for social login?</h2>
<p>First, let's configure Auth.js. This is our "Public Security" layer. It handles the user-facing complexity so our backend doesn't have to.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// src/auth.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> NextAuth </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next-auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> GitHub </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next-auth/providers/github</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> handlers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> signIn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> signOut</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> NextAuth</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  providers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [GitHub]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  callbacks</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    async</span><span style="--shiki-dark:#F07178;--shiki-light:#6F42C1"> jwt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> account</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">      // Persist the OAuth access_token to the token right after signin</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">account</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">accessToken</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> account</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">access_token</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> token</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    async</span><span style="--shiki-dark:#F07178;--shiki-light:#6F42C1"> session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">      // Send properties to the client</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">accessToken</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">accessToken</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> as</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>Crucially, implementing a <a href="https://www.rabinarayanpatra.com/snippets/nextjs/proxy-ts-auth-gate">Next.js 16 auth-gated proxy.ts</a> (formerly <code>middleware.ts</code>) allows us to attach this token to requests headed for Spring.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// src/lib/api-client.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetchFromSpring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">endpoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> auth</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">?.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">accessToken</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">token</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">throw</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Error</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unauthorized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fetch</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(\</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://api.myapp.com</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\$</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{endpoint}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">, {</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">        headers: {</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            'Authorization': </span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Bearer </span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\$</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">{token}</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5">\`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">,</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">            'Content-Type': 'application/json'</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">        }</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">    });</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">    </span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">    return response.json();</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">}</span></span></code></pre></figure>
<h2 id="how-does-spring-security-validate-oauth2-tokens-from-authjs">How does Spring Security validate OAuth2 tokens from Auth.js?</h2>
<p>On the Java side, we don't care about login forms, redirects, or client secrets. We only care about <strong>The Token</strong>.</p>
<p>Spring Security 6 makes this trivial with <code>oauth2ResourceServer</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableWebSecurity</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ResourceServerConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">${spring.security.oauth2.resourceserver.jwt.issuer-uri}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> issuerUri</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> filterChain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        http</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/public/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">permitAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">oauth2ResourceServer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">oauth2 </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> oauth2</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">jwt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">jwt </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jwt</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">JwtDecoders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fromIssuerLocation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">issuerUri</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            );</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="why-is-the-jwk-set-uri-critical-for-token-validation">Why is the JWK Set URI critical for token validation?</h3>
<p>When you set <code>issuer-uri</code> in <code>application.yml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  security</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    oauth2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      resourceserver</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        jwt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          issuer-uri</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> https://token.actions.githubusercontent.com</span></span></code></pre></figure>
<p>Spring Boot automatically:</p>
<ol>
<li>Calls <code>/.well-known/openid-configuration</code>.</li>
<li>Finds the <code>jwks_uri</code>.</li>
<li>Downloads the public keys used to sign the tokens.</li>
<li>Rotates them automatically if the provider changes keys.</li>
</ol>
<h2 id="how-do-you-solve-samesite-cookie-issues-in-bff-architecture">How do you solve SameSite cookie issues in BFF architecture?</h2>
<p>In 2025, browser privacy rules are strict. If your Next.js app is on <code>app.com</code> and Spring is on <code>api.com</code>, you might face cookie issues if you try to share cookies directly.</p>
<p>This is why the <strong>Token Exchange</strong> approach (Bearer Token) is superior for this stack. The cookie stays with Next.js (SameSite=Lax), and only the secure backend channel sees the Bearer token.</p>
<h2 id="how-do-you-relay-tokens-to-downstream-microservices">How do you relay tokens to downstream microservices?</h2>
<p>If Spring needs to call <em>another</em> downstream microservice on behalf of the user (Token Relay), use <code>WebClient</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> WebClient</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> webClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">OAuth2AuthorizedClientManager</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> authorizedClientManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    ServletOAuth2AuthorizedClientExchangeFilterFunction</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> oauth2Client </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ServletOAuth2AuthorizedClientExchangeFilterFunction</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">authorizedClientManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> WebClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">apply</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">oauth2Client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">oauth2Configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="conclusion">Conclusion</h2>
<p>By decoupling the <strong>Identity Provider</strong> (Next.js/Auth.js) from the <strong>Resource Server</strong> (Spring Boot), you get the best of both worlds:</p>
<ul>
<li>A user-friendly, social-login capable frontend.</li>
<li>A stateless, scalable, and secure backend foundation.</li>
</ul>
<p>For further reading, see the <a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html">Spring Security OAuth2 Resource Server Documentation</a>, the <a href="https://authjs.dev/getting-started">Auth.js (NextAuth) Getting Started Guide</a>, and the <a href="https://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core Specification</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution">The Component Revolution: Mastering Spring Security 6.x</a> — Understand the SecurityFilterChain model this guide builds on.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero Trust Architecture in Spring Boot Microservices</a> — Add mTLS and ABAC policies on top of your OAuth2 setup.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">Building a Modern Proxy Server with Next.js 16</a> — Another Next.js 16 deep dive with TypeScript.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[The Component Revolution: Mastering Spring Security 6.x Bean Migration]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution</link>
      <guid>https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution</guid>
      <pubDate>Sun, 01 Feb 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[The definitive guide to migrating from WebSecurityConfigurerAdapter to the SecurityFilterChain bean model. Understand the "why", master the "how", and future-proof your security configuration.]]></description>
      <content:encoded><![CDATA[<p>The landscape of Java security has completely shifted. If you've been working with Spring Boot for more than a few years, your muscle memory probably starts every security config with <code>extends WebSecurityConfigurerAdapter</code>.</p>
<p>I know mine did. For years, that class was my safety blanket.</p>
<p>But in Spring Security 5.7, it was deprecated. And in Spring Security 6? <strong>It's gone.</strong></p>
<p>Now, before you panic (or get annoyed at yet another breaking change), let me tell you: <strong>This is actually a good thing.</strong> It’s not just a rename; it’s a total paradigm shift from <strong>Inheritance</strong> to <strong>Composition</strong>.</p>
<p>In this guide, we’re going to dismantle the old ways and rebuild a modern, component-based security architecture together.</p>
<h2 id="why-did-spring-security-move-from-inheritance-to-composition">Why did Spring Security move from inheritance to composition?</h2>
<p>Let's be real—the legacy configuration was a bit of a monolith. You had one class that controlled <em>everything</em>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// LEGACY: The Monolith</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> SecurityConfig</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> WebSecurityConfigurerAdapter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    protected</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> configure</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // ... mixed concerns ...</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The problem? Well, what if you wanted distinct security rules for your <code>/api/v1/*</code> endpoints (Stateless, JWT) and your <code>/admin/*</code> dashboard (Stateful, Session)? You had to create multiple static classes, deal with complex ordering, and fight the framework. It was a mess.</p>
<h2 id="how-do-you-configure-securityfilterchain-in-spring-security-6x">How do you configure SecurityFilterChain in Spring Security 6.x?</h2>
<p>In this new "Component Revolution," <code>HttpSecurity</code> is just a builder that produces a <code>SecurityFilterChain</code>. You register this chain as a bean. The IoC container handles the rest.</p>
<p>It feels much more like "Spring" than the old way ever did.</p>
<h3 id="side-by-side-comparison">Side-by-Side Comparison</h3>
<p>Let's look at a standard configuration. We want to:</p>
<ol>
<li>Disable CSRF (because who needs it for APIs, right?).</li>
<li>Allow public access to <code>/auth/*</code>.</li>
<li>Lock down everything else.</li>
<li>Go stateless with our sessions.</li>
</ol>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// LEGACY (Spring Boot 2.x)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> LegacySecurityConfig</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> WebSecurityConfigurerAdapter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    protected</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> configure</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">      throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        http</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">csrf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">disable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sessionManagement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sessionCreationPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    SessionCreationPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">STATELESS</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">and</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">antMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/auth/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">permitAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// MODERN (Spring Boot 3.x / Security 6)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableWebSecurity</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ModernSecurityConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> filterChain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">      throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        http</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">csrf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">csrf </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> csrf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">disable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sessionManagement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">session </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> session</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sessionCreationPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    SessionCreationPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">STATELESS</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/auth/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">permitAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            );</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/filter-chain-diagram.webp" alt="Spring Security Filter Chain Diagram" width="1024" height="1024"></p>
<p>Notice the shift to <strong>DSL Lambdas</strong> (<code>csrf -> csrf.disable()</code>). This makes the nesting and configuration scope explicit, improving readability and reducing indentation hell.</p>
<h2 id="how-do-you-use-multiple-securityfilterchains-for-different-endpoints">How do you use multiple SecurityFilterChains for different endpoints?</h2>
<p>The true power comes when you need to mix strategies.</p>
<p>Imagine you have a specialized chain just for your internal actuator endpoints. You want this to strictly check for a specific Role and have a higher precedence.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Runs BEFORE the main chain</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> actuatorSecurity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> throws Exception </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    http</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">securityMatcher</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/actuator/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Only matches these URLs</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasRole</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ADMIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">httpBasic</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDefaults</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Simple auth for monitoring tools</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Fallback chain</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> appSecurity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> throws Exception </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    http</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // ... standard app rules ...</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        )</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // ... JWT filters ...</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="custom-request-matchers">Custom Request Matchers</h2>
<p>In the legacy version, creating custom matching logic was verbose. Now, <code>RequestMatcher</code> is a functional interface.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> customChain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> throws Exception </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    http</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getHeader</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-API-KEY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            ).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">permitAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="conclusion">Conclusion</h2>
<p>The "Component Revolution" in Spring Security forces us to stop thinking about "Configuring the Framework" and start thinking about "Defining Security Beans".</p>
<p>It aligns Spring Security with the rest of the Spring ecosystem: explicit, composable, and easier to test.</p>
<p><em>Upgrading to Spring Boot 3? Don't just <code>Ctrl+C, Ctrl+V</code> your old config. Embrace the chain.</em></p>
<p>For the official migration guide, see the <a href="https://docs.spring.io/spring-security/reference/migration/index.html">Spring Security 6.x Migration Documentation</a> and the <a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html">Spring Security Architecture Reference</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs">Full-Stack Social Identity: Spring Security 6.x + Next.js 16 + Auth.js</a> — Apply the new SecurityFilterChain model to a real OAuth2 Resource Server.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/zero-trust-microservices-spring-security">Zero Trust Architecture in Spring Boot Microservices</a> — Take security further with mTLS and custom AuthorizationManagers.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/ai-driven-anomaly-detection-security">AI-Driven Anomaly Detection with Spring Security</a> — Combine Spring Security events with AI-powered threat detection.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[10 Essential Java Libraries to Reduce Boilerplate Code (Beyond Lombok)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok</link>
      <guid>https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok</guid>
      <pubDate>Mon, 19 Jan 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Stop writing getters, setters, and mappers. Discover the essential Java libraries like MapStruct, Records, Retrofit, and jOOQ that will cut your codebase in half.]]></description>
      <content:encoded><![CDATA[<p>Java has a reputation for being verbose. And let's be honest, it is well-earned.
How many times have you written a connector class that looks like a carbon copy of the entity class? Or a manual HTTP client?</p>
<p><strong>Lombok</strong> is the first step, but it's not the only tool in the box.</p>
<p>Here are <strong>10 essential libraries</strong> that will crush the boilerplate in your application and let you focus on what matters: the business logic.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/java-boilerplate-libraries/cover.webp" alt="The Boilerplate Crusher" width="1024" height="1024"></p>
<h2 id="how-does-mapstruct-eliminate-bean-mapping-boilerplate">How does MapStruct eliminate bean mapping boilerplate?</h2>
<p><strong>The Problem:</strong> You have a <code>UserEntity</code> (database) and a <code>UserDTO</code> (API). Writing <code>dto.setName(entity.getName())</code> fifty times is soulless work.</p>
<p><strong>The Solution:</strong> MapStruct generates this code for you at <strong>compile time</strong>. It is type-safe, follows your naming conventions, and is zero-overhead at runtime (unlike reflection-based mappers).</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Mapper</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    UserMapper</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> INSTANCE </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Mappers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UserMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Mapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">emailAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    UserDTO</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> userToUserDTO</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">User</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/java-boilerplate-libraries/mapstruct-flow.webp" alt="MapStruct Flow Diagram" width="1024" height="1024"></p>
<h2 id="how-do-java-records-replace-boilerplate-data-classes">How do Java Records replace boilerplate data classes?</h2>
<p><strong>The Problem:</strong> Creating a simple immutable data carrier requires private fields, getters, <code>equals()</code>, <code>hashCode()</code>, and <code>toString()</code>.</p>
<p><strong>The Solution:</strong> Native to Java (JDK 14+), Records reduce this to <strong>one line</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// BEFORE (20+ lines of clutter)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Point</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ...</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// AFTER (1 line of beauty)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Point</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> x</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> y</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span></code></pre></figure>
<p><em>Tip: Records replace Lombok's <code>@Value</code> effectively.</em></p>
<h2 id="why-is-retrofit-the-best-choice-for-type-safe-http-clients">Why is Retrofit the best choice for type-safe HTTP clients?</h2>
<p><strong>The Problem:</strong> Using standard <code>HttpClient</code> requires manually building URLs, setting headers, and parsing JSON responses. It's repetitive string manipulation.</p>
<p><strong>The Solution:</strong> Retrofit turns your HTTP API into a simple Java interface.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> GitHubService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">users/{user}/repos</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">  Call</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Repo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> listRepos</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-does-jooq-bring-type-safe-sql-to-java">How does jOOQ bring type-safe SQL to Java?</h2>
<p><strong>The Problem:</strong> JPA (Hibernate) is great for simple saves, but complex queries require messy String concatenation (HQL) or the confusing Criteria API.</p>
<p><strong>The Solution:</strong> jOOQ lets you write SQL in Java that is <strong>type-safe</strong>. If you rename a DB column, your Java code <strong>won't compile</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// No more "Select * from..." strings</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">select</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">AUTHOR</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">FIRST_NAME</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> AUTHOR</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LAST_NAME</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">AUTHOR</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">where</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">AUTHOR</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">YEAR_OF_BIRTH</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">gt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1920</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fetch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<h2 id="how-does-assertj-make-test-assertions-more-readable">How does AssertJ make test assertions more readable?</h2>
<p><strong>The Problem:</strong> Standard JUnit assertions (<code>assertEquals(expected, actual)</code>) are clunky and don't offer great autocomplete.</p>
<p><strong>The Solution:</strong> Fluent assertions that read like English.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// AssertJ</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">startsWith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Jo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualToIgnoringCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<h2 id="how-does-vavr-bring-functional-programming-to-java">How does Vavr bring functional programming to Java?</h2>
<p><strong>The Problem:</strong> Java's standard functional API can be verbose, and handling exceptions in Streams (<code>try-catch</code> inside logic) is ugly.</p>
<p><strong>The Solution:</strong> Vavr brings immutable persistent collections and functional control structures like <code>Try</code>, <code>Option</code>, and <code>Either</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Handling exceptions cleanly</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> /</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">   .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">recover</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">x </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">   .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<h2 id="how-does-testcontainers-enable-real-integration-tests">How does Testcontainers enable real integration tests?</h2>
<p><strong>The Problem:</strong> Setting up a real database for local tests is hard. Mocking the database often hides real bugs (e.g., Postgres-specific SQL errors).</p>
<p><strong>The Solution:</strong> Spins up disposable Docker containers for your tests automatically.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Container</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">PostgreSQLContainer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> container </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres:15</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>For a reusable base class that shares one container across your test suite, see the <a href="https://www.rabinarayanpatra.com/snippets/java/testcontainers-postgres-base">Testcontainers Postgres base class snippet</a>.</p>
<h2 id="how-does-the-immutables-library-generate-builder-patterns">How does the Immutables library generate builder patterns?</h2>
<p><strong>The Problem:</strong> You want immutable objects but need "Builders" or "With" methods (copy-on-write) to modify them efficiently.</p>
<p><strong>The Solution:</strong> A powerful annotation processor that generates comprehensive immutable implementations.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Immutable</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Person</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">  String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  int</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Usage: ImmutablePerson.builder().name("John").age(30).build();</span></span></code></pre></figure>
<h2 id="how-does-apache-commons-lang-handle-null-safety">How does Apache Commons Lang handle null safety?</h2>
<p><strong>The Problem:</strong> Java code is filled with null checks: <code>if (str != null &#x26;&#x26; !str.isEmpty())</code>.</p>
<p><strong>The Solution:</strong> <code>StringUtils</code>, <code>CollectionUtils</code>, and <code>ObjectUtils</code> handle null safety for you.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">StringUtils</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isBlank</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userInput</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Handles null, empty, and whitespace</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-does-spring-data-jpa-eliminate-dao-boilerplate">How does Spring Data JPA eliminate DAO boilerplate?</h2>
<p><strong>The Problem:</strong> Writing DAOs requires implementing the same <code>findById</code>, <code>save</code>, <code>delete</code> methods for every single table.</p>
<p><strong>The Solution:</strong> You define the interface, Spring generates the implementation.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// 0 lines of implementation code needed</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserRepository</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> JpaRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Long</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findByLastName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> lastName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="summary">Summary</h2>
<p>Boilerplate code is where bugs hide. By adopting these libraries, you reduce the surface area for errors and make your codebase easier to read.</p>
<p>Start with <strong>MapStruct</strong> and <strong>Records</strong> today—they offer the highest immediate return on investment.</p>
<p>For official documentation, see <a href="https://mapstruct.org/documentation/stable/reference/html/">MapStruct</a>, <a href="https://www.jooq.org/doc/latest/manual/">jOOQ</a>, <a href="https://square.github.io/retrofit/">Retrofit</a>, <a href="https://assertj.github.io/doc/">AssertJ</a>, <a href="https://docs.vavr.io/">Vavr</a>, <a href="https://testcontainers.com/">Testcontainers</a>, <a href="https://immutables.github.io/">Immutables</a>, <a href="https://commons.apache.org/proper/commons-lang/">Apache Commons Lang</a>, and <a href="https://docs.spring.io/spring-data/jpa/reference/index.html">Spring Data JPA</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Fixing LazyInitializationException in Spring Boot</a> — See how the right libraries and query strategies eliminate one of Hibernate's most common headaches.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide">Integration Testing with Testcontainers in Spring Boot</a> — Put Testcontainers (from this list) into practice with a full integration testing walkthrough.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-timezones-clock-guide">Handling Clock and Timezones Correctly in Java</a> — Another area where the right library choices save you from subtle, hard-to-debug issues.</li>
</ul>
<p>Happy Coding! 🚀</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Fixing LazyInitializationException in Spring Boot: The Right Way]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide</guid>
      <pubDate>Tue, 13 Jan 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[The #1 error in JPA explained. Why open-in-view is bad, and how to use JOIN FETCH, @EntityGraph, and DTOs to fix it permanently.]]></description>
      <content:encoded><![CDATA[<p>It’s 5:00 PM on a Friday. You deploy your code. You hit the API.
<strong>500 Internal Server Error.</strong></p>
<pre><code>org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.User.roles, could not initialize proxy - no Session
</code></pre>
<p>Every Java developer faces this.</p>
<p>Most people Google it, find a StackOverflow answer saying <em>"Just set <code>spring.jpa.open-in-view=true</code>"</em> or <em>"change <code>@OneToMany</code> to <code>FetchType.EAGER</code>"</em>, and move on.</p>
<p><strong>Please don't do that.</strong> You are solving the error but creating a massive performance bottleneck.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/lazy-init-guide/error-timeline.webp" alt="Lazy Initialization Error Timeline" width="1024" height="1024"></p>
<h2 id="why-does-this-happen">Why does this happen?</h2>
<p>Hibernate uses <strong>Proxies</strong>. When you load a <code>User</code>, it doesn't load their <code>Roles</code> list from the database immediately (that's <code>Lazy</code> loading). Instead, it puts a placeholder (Proxy) there.</p>
<ol>
<li><strong>Service Layer (Transaction Open)</strong>: You fetch the User. The Session is alive.</li>
<li><strong>Controller Layer (Transaction Closed)</strong>: The transaction finishes. The Hibernate Session closes. The data is "Detached".</li>
<li><strong>JSON Serialization</strong>: Jackson tries to turn your User into JSON. It calls <code>user.getRoles()</code>.</li>
<li><strong>Boom</strong>: The Proxy tries to call the database to load the roles, but the connection is gone. <code>LazyInitializationException</code>.</li>
</ol>
<h2 id="why-is-enable_lazy_load_no_trans-a-dangerous-workaround">Why is enable_lazy_load_no_trans a dangerous workaround?</h2>
<p>There is a Hibernate property called <code>hibernate.enable_lazy_load_no_trans=true</code>.
<strong>Do not use this.</strong></p>
<p>It tells Hibernate: <em>"If the session is closed, just open a quick temporary new database connection to fetch this one field."</em>
If you are serializing a list of 100 users, this will open 100 tiny database connections. This is the <strong>N+1 Select Problem</strong>, and it will kill your database.</p>
<h2 id="how-does-join-fetch-solve-lazyinitializationexception">How does JOIN FETCH solve LazyInitializationException?</h2>
<p>The most robust solution is to tell Hibernate to fetch the data <strong>eagerly</strong> for this specific query, using <code>JOIN FETCH</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserRepository</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> JpaRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Long</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ❌ Standard findById (Lazy)</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Optional&#x3C;User> findById(Long id);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ✅ Eager Fetch (Good)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findByIdWithRoles</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Param</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This generates <strong>one single SQL query</strong> that gets both User and Roles. No proxies. No errors.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/lazy-init-guide/solution.webp" alt="Join Fetch Solution" width="1024" height="1024"></p>
<h2 id="when-should-you-use-entitygraph-instead-of-join-fetch">When should you use @EntityGraph instead of JOIN FETCH?</h2>
<p>If you don't like writing JPQL strings, Spring Data JPA provides <code>@EntityGraph</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserRepository</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> JpaRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Long</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EntityGraph</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">attributePaths</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">roles</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">address</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This does the exact same thing as <code>JOIN FETCH</code> but purely with annotations. You can even define named EntityGraphs on your entity class to reuse them.</p>
<h2 id="why-are-dto-projections-the-best-solution-for-lazy-loading">Why are DTO projections the best solution for lazy loading?</h2>
<p>Sometimes, you don't even need the Entity. If you are just building an API response, fetch a <strong>DTO (Data Transfer Object)</strong> directly.</p>
<p>Hibernate is smart enough to select only the columns you need.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserRepository</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> JpaRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Long</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Define a simple Java Record</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserSummary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> roleName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Query</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">SELECT new com.example.dto.UserSummary(u.username, r.name) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">           "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">FROM User u JOIN u.roles r WHERE u.id = :id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserSummary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findUserSummary</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Param</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Why this wins</strong>:</p>
<ol>
<li><strong>Zero Lazy Loading issues</strong>: It's just a POJO/Record, not a Hibernate entity.</li>
<li><strong>Performance</strong>: You only select the 2 columns you need, instead of <code>SELECT *</code>.</li>
<li><strong>Safety</strong>: You can't accidentally trigger a LazyInitException because there are no proxies.</li>
</ol>
<h2 id="summary">Summary</h2>
<p><code>LazyInitializationException</code> is not a bug; it's a feature protecting you from loading your entire database into memory.</p>
<ul>
<li><strong>Avoid</strong> <code>OpenEntityManagerInView</code> (it keeps DB connections open too long).</li>
<li><strong>Use</strong> <code>JOIN FETCH</code> or <code>@EntityGraph</code> when you need the Entities.</li>
<li><strong>Use</strong> DTO Projections for read-only API responses.</li>
</ul>
<p>Fix it at the query level, not the config level.</p>
<p>For further reading, consult the <a href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#fetching">Hibernate ORM User Guide on Fetching</a>, the <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/entity-graph.html">Spring Data JPA Reference on EntityGraphs</a>, and the <a href="https://jakarta.ee/specifications/persistence/3.1/">JPA 3.1 Specification</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide">Integration Testing with Testcontainers in Spring Boot</a> — Catch lazy loading issues early by testing against a real database with Testcontainers.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">10 Essential Java Libraries Beyond Lombok</a> — Libraries like MapStruct and Spring Data JPA projections that help you avoid lazy loading traps entirely.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-cors-guide">Solving Spring Boot CORS Errors Once and For All</a> — Another common Spring Boot frustration, solved the right way.</li>
</ul>
<p>Happy Coding! 🚀</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Handling Clock and Timezones Correctly in Java]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/java-timezones-clock-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/java-timezones-clock-guide</guid>
      <pubDate>Wed, 07 Jan 2026 18:30:00 GMT</pubDate>
      <description><![CDATA[Why LocalDateTime.now() destroys your tests and how to use java.time.Clock to handle timezones like a pro in production applications.]]></description>
      <content:encoded><![CDATA[<p>Pop quiz: What is wrong with this code?</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TokenService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> isExpired</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Token</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // ❌ The Villain</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getExpiresAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isBefore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LocalDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>It looks innocent. But <code>LocalDateTime.now()</code> is a hidden dependency on the <strong>server's system clock</strong>.</p>
<ol>
<li><strong>Testing is a nightmare</strong>: How do you test "token expiration" without <code>Thread.sleep()</code>? You can't control <code>now()</code>.</li>
<li><strong>Timezones are ignored</strong>: If your server is in UTC but <code>LocalDateTime.now()</code> picks up the system default (e.g., EST), your logic breaks.</li>
</ol>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/java-timezones-guide/chaos.webp" alt="Timezone Chaos" width="1024" height="1024"></p>
<h2 id="how-does-javatimeclock-solve-the-hidden-time-dependency">How does java.time.Clock solve the hidden time dependency?</h2>
<p>Since Java 8, we have had a built-in abstraction for time: <code>java.time.Clock</code>.</p>
<p>Instead of asking the <em>System</em> for the time, you ask the <em>Clock</em>. And because the Clock is an object, you can inject it.</p>
<h3 id="step-1-define-the-bean">Step 1: Define the Bean</h3>
<p>In your Spring Boot configuration, define a global Clock bean. best practice is to always force UTC.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TimeConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Clock</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> clock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Clock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">systemUTC</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="step-2-inject-it">Step 2: Inject It</h3>
<p>Now, refactor your service to depend on the Clock.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TokenService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ✅ The Hero</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Clock</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> clock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> isExpired</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Token</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Ask the clock for "now"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getExpiresAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isBefore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LocalDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">clock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="step-3-testing-time-travel">Step 3: Testing "Time Travel"</h3>
<p>This is where the magic happens. In your tests, you don't use the system clock. You use a <strong>Fixed Clock</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TokenServiceTest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Test</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> shouldFindExpiredToken</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 1. Freeze time at specific date</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Instant</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fixedInstant </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">parse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">2023-10-01T10:00:00Z</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Clock</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fixedClock </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Clock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fixed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">fixedInstant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ZoneId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">UTC</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        TokenService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> service </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> TokenService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">fixedClock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 2. Create a token that expires 1 second BEFORE the fixed time</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Token</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> token </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setExpiresAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LocalDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofInstant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">fixedInstant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">minusSeconds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ZoneId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">UTC</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 3. Assert (Value is deterministic!)</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">service</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isExpired</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isTrue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>No <code>Thread.sleep()</code>. No flaky tests. Just pure deterministic logic.</p>
<h2 id="why-should-your-production-architecture-always-use-utc">Why should your production architecture always use UTC?</h2>
<p>Dealing with users in Tokyo, London, and New York? Follow this golden rule:</p>
<blockquote>
<p><strong>Servers, APIs, and Databases always speak UTC.</strong></p>
</blockquote>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/java-timezones-guide/architecture.webp" alt="UTC Architecture Flow" width="1024" height="1024"></p>
<h3 id="1-database">1. Database</h3>
<p>Always use <code>TIMESTAMP WITH TIME ZONE</code> (Postgres) or store as UTC <code>DATETIME</code>.
In Java, map this to <code>Instant</code> or <code>OffsetDateTime</code>. <strong>Avoid <code>LocalDateTime</code></strong> for storage as it lacks timezone context.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Entity</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ✅ Good: unambiguous point in timeline</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createdAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ❌ Bad: ambiguous (Is this 10 AM Tokyo or 10 AM NY?)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> LocalDateTime</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> deliveryDue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="2-api-layer">2. API Layer</h3>
<p>Spring Boot (via Jackson) behaves differently depending on configuration. Force it to standard ISO-8601 UTC.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="properties" data-theme="material-theme github-light"><code data-language="properties" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># application.properties</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.jackson.time-zone</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UTC</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#D73A49">spring.jackson.serialization.write-dates-as-timestamps</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">false</span></span></code></pre></figure>
<p>Now your JSON will always look like <code>"2023-10-27T10:00:00Z"</code>.</p>
<h3 id="3-the-frontend">3. The Frontend</h3>
<p>The Browser knows the user's timezone.</p>
<ul>
<li><strong>Server sends</strong>: <code>2023-10-27T10:00:00Z</code></li>
<li><strong>Browser JS</strong>: <code>new Date('2023-10-27T10:00:00Z').toString()</code> -> Displays <code>10:00 PM Tokyo Time</code>.</li>
</ul>
<p>Conversion happens at the <strong>Edge</strong> (the user's screen), never in the core logic.</p>
<h2 id="summary">Summary</h2>
<ol>
<li><strong>Never</strong> use <code>LocalDateTime.now()</code> in business logic.</li>
<li><strong>Always</strong> inject <code>java.time.Clock</code>.</li>
<li><strong>Use</strong> <code>Clock.fixed()</code> in tests to time-travel.</li>
<li><strong>Store</strong> everything as UTC (<code>Instant</code>).</li>
</ol>
<p>Time is hard. But with <code>Clock</code>, at least it's testable.</p>
<p>For further reading, see the <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Clock.html">java.time.Clock Javadoc</a>, the <a href="https://docs.oracle.com/javase/tutorial/datetime/">Java Date and Time API tutorial</a>, and the <a href="https://docs.spring.io/spring-boot/appendix/application-properties/index.html">Spring Boot Jackson datetime configuration reference</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide">Integration Testing with Testcontainers in Spring Boot</a> — Use <code>Clock.fixed()</code> alongside Testcontainers for fully deterministic integration tests.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">10 Essential Java Libraries Beyond Lombok</a> — More libraries that eliminate boilerplate, including testing and utility tools that complement good time handling.</li>
</ul>
<p>Happy Coding! ⏱️</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Integration Testing with TestContainers in Spring Boot: A Practical Handbook]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/spring-boot-testcontainers-guide</guid>
      <pubDate>Thu, 25 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Stop writing brittle tests with H2. Learn how to use Testcontainers to run your integration tests against real Dockerized databases in Spring Boot 3.1+.]]></description>
      <content:encoded><![CDATA[<p>If you are writing integration tests in 2025 and you are still using H2 (an in-memory database), you are doing it wrong.</p>
<p>I know, that sounds harsh. But I’ve seen too many production bugs happen because H2 behaves <em>slightly</em> differently than PostgreSQL. H2 is case-insensitive by default in some modes; Postgres is not. H2 doesn't support JSONB properly; Postgres does.</p>
<p>Testing against a "fake" database gives you a false sense of security.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/testcontainers-guide/mock-vs-real.webp" alt="Mock vs Real Database" width="1024" height="1024"></p>
<p>The solution? <strong>Testcontainers.</strong> If you want the end state up front, the <a href="https://www.rabinarayanpatra.com/snippets/java/testcontainers-postgres-base">reusable Testcontainers Postgres base class snippet</a> is what every integration test in this guide ends up extending.</p>
<h2 id="what-is-testcontainers">What is Testcontainers?</h2>
<p>Testcontainers is a Java library that allows your JUnit tests to spin up <strong>real Docker containers</strong> for your dependencies (Postgres, Redis, Kafka, Elasticsearch, etc.) on the fly.</p>
<p>Instead of mocking your database, you literally boot up a real PostgreSQL instance inside Docker, run your tests against it, and then throw it away.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/testcontainers-guide/architecture.webp" alt="Testcontainers Architecture" width="1024" height="1024"></p>
<h2 id="how-do-you-set-up-testcontainers-with-spring-boot-31">How do you set up Testcontainers with Spring Boot 3.1+?</h2>
<p>Before Spring Boot 3.1, setting up Testcontainers was a bit verbose. You had to manually define <code>@DynamicPropertySource</code> to inject the database URL into your Spring context.</p>
<p><strong>Spring Boot 3.1 changed the game</strong> with a new feature called <code>Service Connections</code>. It automatically configures the connection details for you.</p>
<p>Let's look at the code.</p>
<h3 id="1-add-dependencies">1. Add Dependencies</h3>
<p>First, you need the standard test dependencies plus Testcontainers.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependencies</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    &#x3C;!-- Standard Spring Boot Test --></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">org.springframework.boot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">spring-boot-starter-test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    &#x3C;!-- Testcontainers --></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">org.springframework.boot</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">spring-boot-testcontainers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">org.testcontainers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">postgresql</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">org.testcontainers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">junit-jupiter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">scope</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependencies</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="2-writing-the-test">2. Writing the Test</h3>
<p>Here is the magic. Look how clean this is compared to the old way.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">SpringBootTest</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Testcontainers</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // 1. Enable Testcontainers support</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CustomerRepositoryTest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // 2. Define the Container</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Container</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ServiceConnection</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // 3. The Magic Annotation!</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postgres </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres:16-alpine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CustomerRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customerRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Test</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> shouldSaveAndRetrieveCustomer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Customer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customer </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">John Doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        customerRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Customer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> found </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> customerRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findByEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isPresent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">John Doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Wait, where is <code>spring.datasource.url</code>?
<strong>You don't need it.</strong> The <code>@ServiceConnection</code> annotation tells Spring Boot: <em>"Hey, this is a Postgres container. Please look at its mapped port and automatically configure the datasource to point to it."</em></p>
<p>It just works.</p>
<h2 id="how-do-you-share-a-single-container-across-all-tests">How do you share a single container across all tests?</h2>
<p>The code above works perfectly, but there is a catch: <strong>It starts a new Postgres container for every test class.</strong></p>
<p>If you have 50 test classes, that's 50 Docker startups. Your CI pipeline will take forever.</p>
<p>To fix this, we use the <strong>Singleton Pattern</strong>. We start the container <em>once</em> and share it across all tests.</p>
<p>Base Test Class:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> abstract</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> BaseIntegrationTest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postgres</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Start the container manually in a static block</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        postgres </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres:16-alpine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        postgres</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Still use ServiceConnection for auto-configuration!</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DynamicPropertySource</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> configureProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">DynamicPropertyRegistry</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">       // Manual configuration if not using @ServiceConnection on a static field in the same class</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">       // OR simpler: just use @ServiceConnection on the abstract class if using Boot 3.1+</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // The Modern Singleton Way (Spring Boot 3.1+)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">TestConfiguration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">proxyBeanMethods</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TestContainersConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ServiceConnection</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> postgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres:16-alpine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><em>Author's Note: Actually, there is an even simpler way in Spring Boot 3.1 called <code>TestConfiguration</code> files, but to keep it simple, just know that sharing containers is key to performance.</em></p>
<h2 id="how-do-you-handle-dirty-spring-contexts-with-testcontainers">How do you handle dirty Spring contexts with Testcontainers?</h2>
<p>Since you are reusing the same database, data from <code>Test A</code> might leak into <code>Test B</code>.</p>
<p>You have two options:</p>
<ol>
<li><strong>@Transactional</strong>: Annotate your test methods with <code>@Transactional</code>. Spring will roll back the transaction at the end of the test, leaving the DB clean. (Recommended for most cases).</li>
<li><strong>Manual Cleanup</strong>: <code>customerRepository.deleteAll()</code> in an <code>@AfterEach</code> block.</li>
</ol>
<h2 id="summary">Summary</h2>
<p>Testcontainers has moved from "nice to have" to "essential".</p>
<ul>
<li><strong>Reliability</strong>: You test against the real thing.</li>
<li><strong>Portability</strong>: It works on any machine with Docker (Mac, Windows, Linux, CI).</li>
<li><strong>Simplicity</strong>: With Spring Boot 3.1+, the configuration is almost zero.</li>
</ul>
<p>Stop mocking your database. It deserves better.</p>
<p>For further reading, see the <a href="https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/">Testcontainers official documentation</a>, the <a href="https://docs.spring.io/spring-boot/reference/testing/testcontainers.html">Spring Boot 3.1 Service Connections reference</a>, and the <a href="https://junit.org/junit5/docs/current/user-guide/">JUnit 5 User Guide</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Fixing LazyInitializationException in Spring Boot</a> — The Hibernate pitfalls that Testcontainers help you catch before production.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-timezones-clock-guide">Handling Clock and Timezones Correctly in Java</a> — Combine <code>Clock.fixed()</code> with Testcontainers for fully reproducible, time-sensitive tests.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">10 Essential Java Libraries Beyond Lombok</a> — More productivity-boosting libraries including Testcontainers, AssertJ, and others.</li>
</ul>
<p>Happy testing! 🧪</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Solving Spring Boot CORS Errors Once and For All]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/spring-boot-cors-guide</link>
      <guid>https://www.rabinarayanpatra.com/blogs/spring-boot-cors-guide</guid>
      <pubDate>Sun, 21 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[The definitive guide to fixing Access-Control-Allow-Origin errors in Spring Boot. Learn the difference between @CrossOrigin, Global Config, and Security Filters.]]></description>
      <content:encoded><![CDATA[<p>If you are a full-stack developer, this error haunts your dreams:</p>
<blockquote>
<p><em>Access to fetch at <code>http://localhost:8080/api</code> from origin <code>http://localhost:3000</code> has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.</em></p>
</blockquote>
<p>You just want to connect your React frontend to your Spring Boot backend. Why is the browser stopping you?</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/spring-boot-cors-guide/cors-nightmare.webp" alt="CORS Nightmare Debug" width="1024" height="1024"></p>
<h2 id="what-is-cors-its-not-a-bug">What is CORS? (It's not a bug)</h2>
<p><strong>CORS (Cross-Origin Resource Sharing)</strong> is a security feature protecting the <strong>browser</strong>, not the server.</p>
<p>Without CORS, a malicious site (<code>evil.com</code>) could make a request to <code>bank.com</code> content while you are logged in. The browser blocks cross-origin requests by default unless the server explicitly says <em>"It's okay, I trust <code>evil.com</code>"</em>.</p>
<h3 id="the-preflight-check">The Preflight Check</h3>
<p>Before sending a specialized request (like a <code>POST</code> with JSON), the browser sends a "Preflight" <code>OPTIONS</code> request.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/spring-boot-cors-guide/cors-handshake.webp" alt="CORS Handshake Flow" width="1024" height="1024"></p>
<p>If your server doesn't reply to <code>OPTIONS</code> with <code>200 OK</code> and the correct <code>Access-Control-Allow-Origin</code> headers, the browser <strong>blocks the real request.</strong></p>
<h2 id="solution-1-global-configuration-the-best-way">Solution 1: Global Configuration (The Best Way)</h2>
<p>If you want to apply rules to your entire application, implement <code>WebMvcConfigurer</code>. This is the cleanest approach.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CorsConfig</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> WebMvcConfigurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> addCorsMappings</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CorsRegistry</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Apply to all endpoints</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allowedOrigins</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">http://localhost:3000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://mydomain.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allowedMethods</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">PUT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">DELETE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">OPTIONS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allowedHeaders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">*</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allowCredentials</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">maxAge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">3600</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Cache the preflight response for 1 hour</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Why this is good:</strong> It keeps your configuration central. You define your frontend URL in one place (perfect for pulling from <code>application.properties</code>).</p>
<h2 id="solution-2-crossorigin-the-quick-way">Solution 2: @CrossOrigin (The Quick Way)</h2>
<p>If you only need to open up a single specific endpoint, you can annotate the controller.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/public</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Allow ALL origins (Not recommended for prod)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">CrossOrigin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">origins</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">*</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PublicController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/hello</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> hello</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Hello World</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Why this is bad:</strong> It clutters your business code. Code duplication. If you have 50 controllers, do you paste this 50 times?</p>
<h2 id="solution-3-spring-security-the-gotcha">Solution 3: Spring Security (The "Gotcha")</h2>
<p>If you are using <strong>Spring Security</strong>, the solutions above might <strong>NOT WORK</strong>.</p>
<p>Why? Because Spring Security sits <em>before</em> Spring MVC in the filter chain. It will intercept the <code>OPTIONS</code> request and reject it (401 Unauthorized) before it ever reaches your <code>WebMvcConfigurer</code> settings.</p>
<p>You must configure CORS inside the <code>SecurityFilterChain</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableWebSecurity</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> SecurityConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> filterChain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        http</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // 1. Enable CORS in Security</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">cors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Customizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDefaults</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // 2. Disable CSRF (Usually needed for stateless APIs)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">csrf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">csrf </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> csrf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">disable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            </span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            );</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // 3. Define the source</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CorsConfigurationSource</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> corsConfigurationSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CorsConfiguration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> configuration </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CorsConfiguration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowedOrigins</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">http://localhost:3000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowedMethods</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">PUT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">DELETE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowedHeaders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">*</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowCredentials</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        </span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        UrlBasedCorsConfigurationSource</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> source </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UrlBasedCorsConfigurationSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">registerCorsConfiguration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="summary-checklist">Summary Checklist</h2>
<ol>
<li><strong>Just Spring Boot?</strong> Use <code>WebMvcConfigurer</code> (Solution 1).</li>
<li><strong>Using Spring Security?</strong> Use <code>cors(Customizer.withDefaults())</code> and a <code>CorsConfigurationSource</code> Bean (Solution 3).</li>
<li><strong>Authentication Cookies?</strong> You MUST set <code>.allowCredentials(true)</code> AND you cannot use <code>*</code> for origins. You must specify the exact domain.</li>
</ol>
<p>CORS errors are frustrating, but they are just the browser trying to keep your users safe. Configure it once, centrally, and sleep soundly.</p>
<p>For deeper understanding, see the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">MDN Web Docs on CORS</a>, the <a href="https://docs.spring.io/spring-framework/reference/web/webmvc-cors.html">Spring Framework CORS Support Documentation</a>, and the <a href="https://fetch.spec.whatwg.org/#http-cors-protocol">Fetch Living Standard on CORS Protocol</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/spring-security-component-revolution">The Component Revolution: Mastering Spring Security 6.x Bean Migration</a> — If you're configuring CORS with Spring Security, you'll want to understand the new component-based security model.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs">Full-Stack Social Identity: Spring Security + Next.js + Auth.js</a> — CORS configuration is critical for full-stack auth; see how it fits into a real Spring + Next.js setup.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Fixing LazyInitializationException in Spring Boot</a> — Another Spring Boot configuration headache, explained and solved properly.</li>
</ul>
<p>Happy Coding! ☕</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Goodbye middleware.ts, Hello proxy.ts: The Next.js 16 Migration Guide]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16</link>
      <guid>https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16</guid>
      <pubDate>Fri, 19 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Next.js 16 has killed `middleware.ts`. Learn how to migrate to `proxy.ts`, why auth in middleware is now considered unsafe, and how to master the new Node.js-based proxy layer.]]></description>
      <content:encoded><![CDATA[<p>If you just upgraded to Next.js 16 and your app exploded, you are not alone.</p>
<p>The most controversial breaking change in this release is the complete removal of <code>middleware.ts</code>. It has been replaced by a new primitive: <code>proxy.ts</code>.</p>
<p>And it's not just a rename. The rules have changed.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/nextjs-middleware-mastery/migration.webp" alt="Middleware to Proxy Migration" width="1024" height="1024"></p>
<h2 id="why-did-they-kill-middleware">Why did they kill Middleware?</h2>
<p>For years, developers abused Middleware. We engineered complex authentication flows, database calls, and heavy logic into a layer that was only meant for routing.</p>
<p>This came to a head with <strong>CVE-2025-29927</strong>, a vulnerability where Middleware authentication could be bypassed under high load due to Edge Runtime limitations.</p>
<p>Vercel's response in Next.js 16 is clear: <strong>Network boundaries must be explicit.</strong></p>
<ul>
<li><code>proxy.ts</code> runs on <strong>Node.js</strong> (by default), not the limited Edge Runtime.</li>
<li>It is strictly for <strong>Routing</strong> (Rewrites, Redirects, Headers).</li>
<li>It is <strong>NOT</strong> for Authentication (Auth should happen in Layouts or Route Handlers).</li>
</ul>
<h2 id="how-do-you-migrate-from-middlewarets-to-proxyts-in-nextjs-16">How do you migrate from middleware.ts to proxy.ts in Next.js 16?</h2>
<h3 id="1-file-rename--location">1. File Rename &#x26; Location</h3>
<ul>
<li><strong>Old:</strong> <code>middleware.ts</code> in root or <code>src/</code>.</li>
<li><strong>New:</strong> <code>app/proxy.ts</code> (Must be inside the App Router directory).</li>
</ul>
<h3 id="2-syntax-changes">2. Syntax Changes</h3>
<p>The signature looks similar, but notice the function name and runtime behavior.</p>
<p><strong>The Legacy Way (<code>middleware.ts</code>):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ❌ middleware.ts (Legacy)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> NextResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next/server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> NextRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next/server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> middleware</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> NextRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // We used to do Auth here... dangerous!</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cookies</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">token</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">token</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> NextResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">redirect</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> URL</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/login</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">url</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> NextResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">next</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>The Next.js 16 Way (<code>proxy.ts</code>):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ✅ app/proxy.ts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ProxyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ProxyResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next/server</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Note: It's named 'proxy', not 'middleware'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> proxy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ProxyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Promise</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">ProxyResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> pathname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">nextUrl</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // 1. Rewrites (A/B Testing, Localization)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pathname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ===</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/about</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">     return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ProxyResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">rewrite</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> URL</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/fr/about</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">url</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // 2. Redirects (Legacy paths)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pathname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">startsWith</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/old-blog</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">      return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ProxyResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">redirect</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> URL</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/blog</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">url</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // 3. Headers (Security)</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // You can now access full Node.js APIs here if needed!</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ProxyResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">next</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  res</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">X-Frame-Options</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">DENY</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> res</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<blockquote>
<p>[!WARNING]
<strong>Do not attempt to read databases or verify complex JWTs in <code>proxy.ts</code>.</strong> While it runs on Node.js and technically <em>can</em> connect to a DB, blocking the request at the proxy level adds significant latency to your TTFB (Time To First Byte) for <em>every single request</em>.</p>
</blockquote>
<h2 id="where-does-auth-go-now">Where does Auth go now?</h2>
<p>If <code>proxy.ts</code> is just for routing, where do we protect our routes?</p>
<p>Next.js 16 introduces <strong>Server Layout Guards</strong>.</p>
<p>Instead of a global middleware file, you wrap your protected routes in a <code>layout.tsx</code> that performs the check. This leverages React Server Components (RSC) to handle security closer to the data.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// app/dashboard/layout.tsx</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> redirect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">next/navigation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">import</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> verifySession</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@/lib/auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Your standard server-side auth</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> DashboardLayout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">({</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> children</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> })</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> await</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> verifySession</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  if</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">session</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    redirect</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/login</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Server-side redirect</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">section</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        {</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">children</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    &#x3C;/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">section</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">  )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>For a working auth-gate template you can copy into your project, see the <a href="https://www.rabinarayanpatra.com/snippets/nextjs/proxy-ts-auth-gate">Auth-Gated proxy.ts snippet</a>.</p>
<h2 id="what-are-production-ready-proxyts-patterns-for-nextjs-16">What are production-ready proxy.ts patterns for Next.js 16?</h2>
<p>Here are the safe, approved patterns for the new Proxy layer.</p>
<h3 id="1-the-localizer-geo-routing">1. The Localizer (Geo-Routing)</h3>
<p>Since <code>proxy.ts</code> has full Node.js access, you can use powerful libraries like <code>maxmind</code> directly if you want, but the standard <code>request.geo</code> (on Vercel) is still the fastest.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> proxy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ProxyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> country</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">geo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">?.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">country</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ||</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">US</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> locale</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getLocaleFromCountry</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">country</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">) </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// e.g., 'fr-FR'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Transparent rewrite</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ProxyResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">rewrite</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> URL</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">locale</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">nextUrl</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">pathname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">url</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="2-header-injection-csp--security">2. Header Injection (CSP &#x26; Security)</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">export</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> proxy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ProxyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> nonce</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> crypto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">randomUUID</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">() </span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Valid Node.js crypto!</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Headers</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">headers</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">x-nonce</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> nonce</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ProxyResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">next</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#24292E">    request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Content-Security-Policy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> `</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">script-src 'self' 'nonce-</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">${</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">nonce</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">}</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">'</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">`</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="summary">Summary</h2>
<p>The death of <code>middleware.ts</code> is painful but necessary. It forces us to decouple <strong>Routing</strong> (Proxy) from <strong>Security</strong> (Layouts/RSC).</p>
<p><strong>Checklist for migration:</strong></p>
<ol class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> Rename <code>middleware.ts</code> to <code>app/proxy.ts</code>.</li>
<li class="task-list-item"><input type="checkbox" disabled> Rename exported function to <code>proxy</code>.</li>
<li class="task-list-item"><input type="checkbox" disabled> <strong>CRITICAL:</strong> Move all Authentication logic OUT of the proxy and into <code>layout.tsx</code> or Server Actions.</li>
<li class="task-list-item"><input type="checkbox" disabled> Celebrate your faster, more secure app.</li>
</ol>
<p>Happy coding, and welcome to the Next.js 16 era. 🚀</p>
<h2 id="references--further-reading">References &#x26; Further Reading</h2>
<p>For those who want to verify the details or read the full specs, here are the official sources:</p>
<ul>
<li><strong><a href="https://nvd.nist.gov/vuln/detail/CVE-2025-29927">CVE-2025-29927</a></strong>: The vulnerability details regarding Middleware authentication bypass.</li>
<li><strong><a href="https://nextjs.org/docs/app/building-your-application/upgrading/codemods#16-proxy-migration">Next.js 16 Upgrade Guide</a></strong>: The official codemod for migrating to <code>proxy.ts</code>.</li>
<li><strong><a href="https://vercel.com/docs/edge-network/proxy">Vercel Proxy Documentation</a></strong>: Deep dive into the Node.js Proxy runtime capabilities.</li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a Modern Documentation Generator with Next.js 16</a> — A real-world Next.js 16 project that uses the new proxy.ts architecture.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet">The Day a React Patch Broke the Internet</a> — The security vulnerability that forced the middleware-to-proxy migration in the first place.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/full-stack-social-identity-spring-nextjs">Full-Stack Social Identity: Spring Security + Next.js + Auth.js</a> — How authentication works in the post-middleware Next.js world with Spring Security on the backend.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Debezium Outbox Pattern in Spring Boot: Worked Example]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices</link>
      <guid>https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices</guid>
      <pubDate>Wed, 10 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Production walkthrough of the Debezium outbox pattern in Spring Boot. Worked example with code, Kafka topics, and the dual-write problem solved end to end.]]></description>
      <content:encoded><![CDATA[<p>You're building a microservice. It's simple: a user places an order, you save it to the database, and then you publish an <code>OrderCreated</code> event to Kafka so the Shipping Service can do its job.</p>
<p>Easy, right?</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Transactional</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createOrder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Order</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // 1. Save to Database</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    orderRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // 2. Publish to Kafka</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    kafkaTemplate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">send</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">orders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Wrong.</strong></p>
<p>This code has a fatal flaw known as the <strong>Dual Write Problem</strong>.</p>
<h2 id="what-is-the-dual-write-problem-in-microservices">What is the Dual Write Problem in microservices?</h2>
<p>Here is the nightmare scenario:</p>
<ol>
<li>Your code commits the transaction to the database. <strong>Success.</strong> The order is created.</li>
<li>Immediately after, your server crashes. Or the network blips. Or Kafka is down.</li>
<li>The event is <strong>never published</strong>.</li>
</ol>
<p>The result? You have an order in your database, but the Shipping Service never knows about it. The customer pays, but the package never ships. Data inconsistency.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/outbox-pattern-cdc/dual-write-problem.webp" alt="Dual Write Problem Diagram" width="1024" height="1024"></p>
<p>You might think, <em>"I'll just reverse it! Publish to Kafka first, then save to DB!"</em>
Nope. What if the Kafka publish succeeds, but the database save fails (constraint violation)? Now Shipping is trying to ship an order that doesn't exist.</p>
<p>You cannot treat a database transaction and a message broker publish as a single atomic unit (unless you want to use Two-Phase Commit / XA Transactions, which essentially kill performance and availability).</p>
<p>So, how do we solve this? Enter the <strong>Transactional Outbox Pattern</strong>. If you want the relayer code straight away, the <a href="https://www.rabinarayanpatra.com/snippets/java/outbox-publisher">Outbox Publisher snippet</a> is the JDBC <code>SKIP LOCKED</code> poller I run in production.</p>
<h2 id="how-does-the-transactional-outbox-pattern-solve-data-consistency">How does the Transactional Outbox Pattern solve data consistency?</h2>
<p>The Outbox Pattern is elegantly simple. Instead of sending the message directly to Kafka, you save it to a database table <strong>in the same transaction</strong> as your business data.</p>
<p>Here is the new flow:</p>
<ol>
<li><strong>Begin Transaction</strong>.</li>
<li>Save <code>Order</code> to the <code>orders</code> table.</li>
<li>Save <code>OrderCreated</code> event to an <code>outbox</code> table.</li>
<li><strong>Commit Transaction</strong>.</li>
</ol>
<p>Because this is a single ACID transaction within your database, it is atomic. Either both happen, or neither happens. No more inconsistency.</p>
<p>But wait—the message is now stuck in a database table. How does it get to Kafka?</p>
<p>That's where <strong>CDC (Change Data Capture)</strong> comes in.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/outbox-pattern-cdc/outbox-pattern-architecture.webp" alt="Transactional Outbox Pattern Architecture" width="1024" height="1024"></p>
<h2 id="why-use-cdc-instead-of-polling-for-the-outbox-pattern">Why use CDC instead of polling for the Outbox Pattern?</h2>
<p>Effectively, we need a "Message Relay" process. There are two ways to do this:</p>
<h3 id="1-the-polling-approach-the-old-way">1. The "Polling" Approach (The Old Way)</h3>
<p>You write a cron job that runs every second:
<code>SELECT * FROM outbox WHERE processed = false</code>
Then it loops through them, publishes to Kafka, and updates them to <code>processed = true</code>.</p>
<p><strong>The Problem:</strong></p>
<ul>
<li><strong>Latency:</strong> You depend on the polling interval.</li>
<li><strong>Database Load:</strong> Constant polling hammers your database, even when empty.</li>
<li><strong>Complexity:</strong> You have to handle locking so multiple instances don't process the same message.</li>
</ul>
<h3 id="2-the-cdc-approach-the-pro-way">2. The CDC Approach (The "Pro" Way)</h3>
<p>Tools like <strong>Debezium</strong> act as a log reader. They hook directly into your database's transaction log (Write-Ahead Log in Postgres, Binlog in MySQL).</p>
<p>When you commit a row to the <code>outbox</code> table, the database writes to its log. Debezium sees this instantly and pushes the change to Kafka.</p>
<ul>
<li><strong>Zero Polling:</strong> It pushes events as they happen.</li>
<li><strong>Zero Database Load:</strong> It reads the log files, not the data tables.</li>
<li><strong>Reliable:</strong> If the connector crashes, it resumes from the exact log position where it left off.</li>
</ul>
<h2 id="how-do-you-implement-the-outbox-pattern-with-spring-boot-and-debezium">How do you implement the Outbox Pattern with Spring Boot and Debezium?</h2>
<p>Let's build this.</p>
<h3 id="step-1-the-outbox-table">Step 1: The Outbox Table</h3>
<p>First, create a table to hold your events.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="sql" data-theme="material-theme github-light"><code data-language="sql" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">CREATE</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> TABLE</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> outbox</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    id uuid </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> PRIMARY KEY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    aggregate_type </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">varchar</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">255</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    aggregate_id </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">varchar</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">255</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">    type</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> varchar</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">255</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    payload jsonb </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">NOT NULL</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    created_at </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">timestamp</span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49"> NOT NULL</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<h3 id="step-2-the-service-implementation">Step 2: The Service Implementation</h3>
<p>In your Spring Boot application, your service now looks like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> OrderService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> OrderRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> OutboxRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> outboxRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ObjectMapper</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> objectMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Transactional</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Order</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createOrder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">OrderRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 1. Create and Save the Order (Business Logic)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Order</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        orderRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 2. Create the Event</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        OrderCreatedEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> OrderCreatedEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getTotal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // 3. Save to Outbox (Same Transaction!)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        OutboxEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> outboxEvent </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> OutboxEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UUID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">randomUUID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">aggregateType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ORDER</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">aggregateId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ORDER_CREATED</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">payload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">objectMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">valueToTree</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Store as JSON</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createdAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        outboxRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">outboxEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That's it for the Java code. We don't touch Kafka here. The transaction commits, and we are safe.</p>
<h3 id="step-3-configuring-debezium">Step 3: Configuring Debezium</h3>
<p>You don't write Java code for Debezium; usually, you deploy it as a Kafka Connect container. Here is a sample configuration (JSON) to tell Debezium to watch your database:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="json" data-theme="material-theme github-light"><code data-language="json" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">order-outbox-connector</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">  "</span><span style="--shiki-dark:#C792EA;--shiki-light:#005CC5">config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">connector.class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">io.debezium.connector.postgresql.PostgresConnector</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.hostname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">5432</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.dbname</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">orderdb</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">database.server.name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">order-service-db</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">table.include.list</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">public.outbox</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">plugin.name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">pgoutput</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">outbox</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">io.debezium.transforms.outbox.EventRouter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">    "</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">transforms.outbox.table.fields.additional.placement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">type:header:eventType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Key Magic:</strong> The <code>transforms.outbox</code> line. Debezium has a specific <strong>SMT (Single Message Transform)</strong> designed exactly for the Outbox pattern.</p>
<ul>
<li>It reads the <code>payload</code> column and sends <em>that</em> as the Kafka message body.</li>
<li>It takes the <code>aggregate_id</code> and uses it as the Kafka Record Key (ensuring ordering).</li>
<li>It takes the <code>type</code> and puts it in the Kafka header.</li>
</ul>
<h3 id="step-4-consuming-the-event">Step 4: Consuming the Event</h3>
<p>Now, your downstream services just listen to the Kafka topic.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">KafkaListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">topics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">outbox.event.order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">shipping-service</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleOrderEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Payload</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                             @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Header</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">eventType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> eventType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ORDER_CREATED</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">eventType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        OrderCreatedEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> objectMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">readValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">payload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> OrderCreatedEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        shippingService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">scheduleShipment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-are-the-trade-offs-of-using-the-outbox-pattern-with-cdc">What are the trade-offs of using the Outbox Pattern with CDC?</h2>
<p>While this architecture is robust, there are a few things to keep in mind:</p>
<ol>
<li><strong>At-Least-Once Delivery</strong>: Debezium guarantees that usage messages will be delivered at least once. It does <em>not</em> guarantee exactly-once. Your consumers (the shipping service) <strong>must be idempotent</strong>. If they receive the same "Order Created" message twice, they shouldn't ship two packages.</li>
<li><strong>Order of Events</strong>: Because we are reading the transaction log, events are naturally ordered. If you create an order and then immediately update it, Debezium will see the INSERT followed by the UPDATE in the correct sequence.</li>
<li><strong>Cleaning Up</strong>: Your <code>outbox</code> table will grow effectively forever. You need a strategy to clean it.
<ul>
<li><strong>Delete after read</strong>: Debezium can be configured to delete the row right after processing it? (Tricky with the Transaction log).</li>
<li><strong>TTL / Cron job</strong>: Just run a daily job: <code>DELETE FROM outbox WHERE created_at &#x3C; NOW() - INTERVAL '3 DAYS'</code>. Since the data is in Kafka, the table is just a temporary buffer.</li>
</ul>
</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>The Dual Write problem is one of those distributed system gotchas that bites everyone at least once.</p>
<p>Using the <strong>Transactional Outbox Pattern</strong> with <strong>CDC</strong> turns a distributed transaction problem into a local database transaction problem—which databases are really, really good at solving.</p>
<p>It forces a strict consistency model: <strong>If it's in the database, it will be in Kafka.</strong></p>
<p>Implementation might look like "over-engineering" at first compared to a simple <code>kafka.send()</code>, but when your production database goes down and comes back up, and you realize you haven't lost a single event? That peace of mind is worth every line of config.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://debezium.io/documentation/">Debezium Documentation</a> — Official CDC platform documentation</li>
<li><a href="https://debezium.io/documentation/reference/transformations/outbox-event-router.html">Debezium Outbox Event Router</a> — The specific SMT for the Outbox Pattern</li>
<li><a href="https://microservices.io/patterns/data/transactional-outbox.html">Chris Richardson's Microservices Patterns</a> — Transactional Outbox pattern description</li>
<li><a href="https://kafka.apache.org/documentation/">Apache Kafka Documentation</a> — Message broker documentation</li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/system-design-question-failed-candidates-2025">The System Design Question That Failed 80% of Candidates</a> — The Outbox Pattern is exactly the kind of deep knowledge that separates candidates in system design interviews.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Mastering Virtual Threads in Java 25</a> — Virtual Threads can dramatically improve the throughput of your CDC consumers and microservice endpoints.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Sanitizer-Lib is Now Live on Maven Central]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/sanitizer-lib-now-on-maven-central</link>
      <guid>https://www.rabinarayanpatra.com/blogs/sanitizer-lib-now-on-maven-central</guid>
      <pubDate>Mon, 08 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Great news for Java developers! Sanitizer-Lib, the library that eliminates input sanitization boilerplate, is now officially available on Maven Central. Here is how to add it to your project.]]></description>
      <content:encoded><![CDATA[<p>It's official: <strong>Sanitizer-Lib</strong> is now available on Maven Central! 🚀</p>
<p>A few months ago, I introduced <a href="https://www.rabinarayanpatra.com/blogs/sanitizer-lib-intro">Sanitizer-Lib</a>, a Java library designed to kill the boilerplate of input sanitization. No more manual <code>.trim()</code> calls, no more scattered string manipulation logic. Just clean, declarative annotations.</p>
<p>Since then, the feedback has been amazing. But there was one friction point: you had to use JitPack or build it locally.</p>
<p><strong>Not anymore.</strong></p>
<p>As of today, you can pull <strong>Sanitizer-Lib</strong> directly from Maven Central. It's production-ready, signed, and just a copy-paste away.</p>
<h2 id="how-do-you-add-sanitizer-lib-to-your-maven-or-gradle-project">How do you add Sanitizer-Lib to your Maven or Gradle project?</h2>
<h3 id="maven">Maven</h3>
<p>Add this to your <code>pom.xml</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.github.rabinarayanpatra.sanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">sanitizer-spring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">1.0.22</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="gradle">Gradle</h3>
<p>Add this to your <code>build.gradle</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="kotlin" data-theme="material-theme github-light"><code data-language="kotlin" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">implementation</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"io.github.rabinarayanpatra:sanitizer-spring:1.0.22"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">)</span></span></code></pre></figure>
<h2 id="what-problems-does-sanitizer-lib-solve-that-manual-validation-doesnt">What problems does Sanitizer-Lib solve that manual validation doesn't?</h2>
<p>If you missed the <a href="https://www.rabinarayanpatra.com/blogs/sanitizer-lib-intro">original deep dive</a>, here is the 30-second pitch:</p>
<p>Instead of writing this validation spaghetti:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">UserDto</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">trim</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toLowerCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ... repeat for 10 other fields</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>You just do this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserDto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> LowerCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That's it. Incoming requests are automatically sanitized <em>before</em> they even hit your controller logic.</p>
<h3 id="what-changed-from-jitpack-to-maven-central">What changed from JitPack to Maven Central?</h3>
<p>Publishing to Maven Central means:</p>
<ul>
<li><strong>No extra repository configuration</strong> — Maven Central is the default repository for every Maven and Gradle project</li>
<li><strong>Signed artifacts</strong> — All JARs are GPG-signed, ensuring you're getting the authentic library</li>
<li><strong>Reliable availability</strong> — Maven Central has 99.99% uptime, unlike JitPack which can have intermittent issues</li>
<li><strong>Better IDE support</strong> — IntelliJ and Eclipse resolve Maven Central dependencies faster</li>
</ul>
<h3 id="migration-from-jitpack">Migration from JitPack</h3>
<p>If you were using Sanitizer-Lib via JitPack, here's what to change:</p>
<ol>
<li>Remove the JitPack repository from your <code>pom.xml</code> or <code>build.gradle</code></li>
<li>Update the group ID from <code>com.github.rabinarayanpatra</code> to <code>io.github.rabinarayanpatra.sanitizer</code></li>
<li>Update to the latest version (<code>1.0.22</code>)</li>
</ol>
<p>The API remains 100% backward compatible — no code changes required.</p>
<h2 id="where-can-you-learn-more-about-sanitizer-lib">Where can you learn more about Sanitizer-Lib?</h2>
<p>For a full walkthrough of features, custom sanitizers, and Spring Boot integration, check out my detailed guide:</p>
<p><strong><a href="https://www.rabinarayanpatra.com/blogs/sanitizer-lib-intro">Read the Full Sanitizer-Lib Introduction</a></strong></p>
<p>Or star the repo on GitHub: <a href="https://github.com/rabinarayanpatra/sanitizer-lib">github.com/rabinarayanpatra/sanitizer-lib</a></p>
<p>For more information, see the <a href="https://search.maven.org/artifact/io.github.rabinarayanpatra.sanitizer/sanitizer-spring">Maven Central Repository Search</a> and the <a href="https://central.sonatype.org/publish/publish-guide/">Sonatype OSSRH Publishing Guide</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/sanitizer-lib-intro">Sanitizer-Lib: The Full Introduction</a> — A complete walkthrough of all features, custom sanitizers, and Spring Boot integration.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">10 Essential Java Libraries Beyond Lombok</a> — More libraries that cut boilerplate and improve code quality alongside Sanitizer-Lib.</li>
</ul>
<p>Happy coding!</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[The Day a React Patch Broke the Internet]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet</link>
      <guid>https://www.rabinarayanpatra.com/blogs/the-day-react-patch-broke-the-internet</guid>
      <pubDate>Mon, 08 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[If you tried to open X (Twitter), Canva, or Discord on December 5th, you likely saw a 500 error. The internet didn't just blink; it stumbled hard. Here is the technical breakdown of the React2Shell outage.]]></description>
      <content:encoded><![CDATA[<p>If you tried to open X (Twitter), Canva, or Discord on December 5th, you likely saw a 500 error. The internet didn't just blink; it stumbled hard.</p>
<p>The irony? The outage wasn't caused by a massive DDoS attack or a hacker group. It was caused by the <strong>defense</strong> against one.</p>
<p>As engineers, we often talk about "Blast Radius" and "Canary Deployments." The recent Cloudflare incident is a masterclass in how fragile global infrastructure can be—and why a single line of Lua code can take down 28% of the world's HTTP traffic.</p>
<p>Here is the technical breakdown of the <strong>React2Shell</strong> vulnerability and the Cloudflare patch that went wrong.</p>
<h2 id="what-was-the-react2shell-vulnerability-cve-2025-55182">What was the React2Shell vulnerability (CVE-2025-55182)?</h2>
<p>Before we get to the outage, we need to understand the panic. On December 3, 2025, a critical vulnerability (CVSS 10.0) was disclosed in <strong>React Server Components (RSC)</strong>.</p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/react2shell-cve.webp" alt="CVE-2025-55182 React2Shell Vulnerability Report" width="1024" height="1024"></p>
<ul>
<li><strong>The Vulnerability:</strong> Dubbed "React2Shell," it affects React 19 and Next.js (versions 15.x and 16.x).</li>
<li><strong>The Exploit:</strong> It allows unauthenticated Remote Code Execution (RCE). An attacker can send a specially crafted HTTP request to a server using the "Flight" protocol (used by RSC) and execute arbitrary code.</li>
<li><strong>The Threat:</strong> Because this exploits the <em>deserialization</em> of data on the server, standard firewalls often miss it. It looks like legitimate traffic until it hits the React server logic.</li>
</ul>
<p>Cloudflare, acting as the shield for millions of websites, decided to roll out a global WAF (Web Application Firewall) rule to block these malicious payloads before they could reach customer servers.</p>
<h2 id="how-did-cloudflares-emergency-patch-make-the-outage-worse">How did Cloudflare's emergency patch make the outage worse?</h2>
<p><strong>Timeline:</strong> December 5, 2025, around 08:47 UTC.</p>
<p>Cloudflare engineers needed to inspect the <em>bodies</em> of incoming requests to detect the React2Shell exploit pattern. To do this effectively for Next.js applications, they needed to increase the WAF's request body buffer size to <strong>1MB</strong> (matching the Next.js default).</p>
<p>Here is the sequence of events that led to disaster:</p>
<ol>
<li><strong>The Config Change:</strong> They deployed a change to increase the buffer size to 1MB.</li>
<li><strong>The Conflict:</strong> They realized an <em>internal</em> WAF testing tool didn't support this larger buffer size. Since the tool wasn't critical for customer traffic, they deployed a second change to <strong>disable the test tool</strong>.</li>
<li><strong>The Lurking Bug:</strong> This is where it gets technical. Disabling that tool triggered a dormant bug in their request routing logic (written in Lua).</li>
<li><strong>The Crash:</strong> The code attempted to access a field called <code>execute</code> on a module that was now <code>nil</code> (null) because the test tool was disabled.</li>
</ol>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/react2shell-diagram.webp" alt="Diagram showing User to Cloudflare Edge to Lua Error" width="1024" height="1024"></p>
<h2 id="what-lua-exception-caused-28-of-global-traffic-to-drop">What Lua exception caused 28% of global traffic to drop?</h2>
<p>According to Cloudflare's post-mortem, the specific error was a <code>nil</code> pointer exception in their NGINX-based proxy (FL1):</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="lua" data-theme="material-theme github-light"><code data-language="lua" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Failed to run </span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">module</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> rulesets callback </span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">late_routing</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">usr</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">/local/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">nginx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">fl</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lua</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">modules</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">init.</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">lua</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">314</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">attempt to index </span><span style="--shiki-dark:#82AAFF;--shiki-light:#005CC5">field</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> '</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">' </span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">(a </span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">nil</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> value)</span></span></code></pre></figure>
<p><strong>Translation for Java/JS Devs:</strong>
Imagine you have a <code>try-catch</code> block, but the logic <em>inside</em> the <code>try</code> block assumes a service is always instantiated. When they toggled the "test tool" off, the service became <code>null</code>. The code tried to run <code>service.execute()</code>, threw an unhandled exception, and returned a <strong>HTTP 500</strong> to the user.</p>
<p>Because this logic sits at the "edge" (the very first point of contact for traffic), the request died instantly. It didn't matter if your backend was up; Cloudflare couldn't route the request to you.</p>
<h2 id="how-much-of-the-internet-went-down-and-for-how-long">How much of the internet went down and for how long?</h2>
<p>For about 25 minutes, chaos ensued.</p>
<ul>
<li><strong>28% of global HTTP traffic</strong> served by Cloudflare returned 500 errors.</li>
<li><strong>Impacted Services:</strong> X (Twitter), LinkedIn, Canva, Discord, and thousands of Next.js apps hosted on Vercel (which uses Cloudflare under the hood).</li>
<li><strong>Resolution:</strong> Cloudflare identified the Lua error and reverted the change by 09:12 UTC.</li>
</ul>
<h2 id="what-should-every-developer-learn-from-the-react2shell-incident">What should every developer learn from the React2Shell incident?</h2>
<p><strong>1. The "Null Pointer" is Still the Billion Dollar Mistake</strong>
It doesn't matter if it's Java, JavaScript, or Lua. Unchecked null/nil references are the #1 cause of sudden death in production. In TypeScript/Next.js, this is why we use Optional Chaining (<code>?.</code>) excessively.</p>
<ul>
<li><em>Takeaway:</em> Never assume a config object or service exists just because it did yesterday.</li>
</ul>
<p><strong>2. Test Your "Kill Switches"</strong>
Cloudflare broke because they turned <em>off</em> a testing tool. We often test our features, but we rarely test the <em>removal</em> of a feature.</p>
<ul>
<li><em>Takeaway:</em> If you have a feature flag to disable a module, test what happens when that flag is actually set to <code>false</code> in a staging environment first.</li>
</ul>
<p><strong>3. Infrastructure as Code (IaC) is Scary</strong>
We are moving toward a world where a single config file controls the security of the entire internet.</p>
<ul>
<li><em>Takeaway:</em> If you are building the "Visual Docker Compose" tool (like I am currently), ensure you have validation layers. A bad config shouldn't just fail; it should fail <em>safely</em> (fail open or fail closed, depending on security needs).</li>
</ul>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>The React2Shell vulnerability is serious. If you are running Next.js 15 or 16, <strong>update immediately</strong> to the patched versions (Next.js 15.5.7+ or 16.0.7+).</p>
<p>Cloudflare took a bullet for us by trying to patch it globally, but they tripped on their own shoelaces. It's a humbling reminder that even the giants of the web are just one <code>nil</code> value away from a blackout.</p>
<h2 id="-sources--references">📚 Sources &#x26; References</h2>
<ul>
<li><strong>Cloudflare Official Post-Mortem:</strong> <a href="https://blog.cloudflare.com/5-december-2025-outage/">Cloudflare outage on December 5, 2025</a></li>
<li><strong>Vulnerability Details:</strong> <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-55182">CVE-2025-55182</a></li>
<li><strong>News Coverage:</strong>
<ul>
<li><a href="https://www.theguardian.com/technology/2025/dec/05/another-cloudflare-outage-takes-down-websites-linkedin-zoom">The Guardian: Cloudflare apologises after latest outage</a></li>
<li><a href="https://www.reuters.com/technology/cloudflare-restores-services-after-minor-dashboard-outage-2025-12-05/">Reuters: Cloudflare restores services after minor dashboard outage</a></li>
<li><a href="https://apnews.com/article/internet-outage-cloudflare-zoom-linkedin-2ac314f7dcd112a63eb12b30afb74a33">AP News: Internet outage hits Zoom, LinkedIn</a></li>
</ul>
</li>
<li><strong>Next.js Security Advisory:</strong> <a href="https://vercel.com/kb/bulletin/react2shell">Vercel Security Bulletin</a></li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">Goodbye middleware.ts, Hello proxy.ts: The Next.js 16 Migration Guide</a> — The architectural overhaul Next.js 16 introduced in response to vulnerabilities like this one.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/system-design-question-failed-candidates-2025">The System Design Question That Failed 80% of Candidates</a> — Infrastructure failures like this Cloudflare outage are exactly the kind of scenario system design interviews probe.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[JSON to DTO Converter: Instantly Generate Java Classes from JSON]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/json-to-dto-converter</link>
      <guid>https://www.rabinarayanpatra.com/blogs/json-to-dto-converter</guid>
      <pubDate>Wed, 03 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Free online JSON to DTO converter. Instantly generate production-ready Java Records, Lombok classes, and Jackson-annotated DTOs from any JSON payload.]]></description>
      <content:encoded><![CDATA[<p>If you're a Java developer, you know the pain. You get a massive JSON response from an API, and now you have to spend the next 20 minutes creating a matching Java class. You have to type out private fields, generate getters and setters, maybe add <code>toString</code> methods, and then realize you made a typo in one of the field names so the mapping fails.</p>
<p>It's boring, repetitive work. And honestly, we have better things to do.</p>
<p>That's why I built <strong>JSON to DTO</strong> – a simple, no-nonsense tool to instantly convert JSON into production-ready Java code.</p>
<p>Check it out here: <a href="https://www.rabinarayanpatra.com/tools/json-to-dto/">https://www.rabinarayanpatra.com/tools/json-to-dto/</a></p>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/json-to-dto/main-interface.webp" alt="JSON to DTO Main Interface" width="1600" height="1179"></p>
<h2 id="why-did-i-build-a-json-to-dto-converter">Why did I build a JSON to DTO converter?</h2>
<p>I was working on a project that involved integrating with several third-party APIs. Every time the API spec changed or I had to integrate a new endpoint, I found myself manually mapping JSON fields to Java classes. I tried a few existing online tools, but they were either too cluttered, outdated (generating old-school Java beans), or didn't support the libraries I actually use, like Lombok or Jackson.</p>
<p>I wanted something clean, fast, and modern. So I built it.</p>
<h2 id="what-features-does-the-json-to-dto-converter-offer">What features does the JSON to DTO converter offer?</h2>
<p>The goal was to make this as "copy-paste" friendly as possible. Here is what it can do:</p>
<h3 id="1-instant-conversion">1. Instant Conversion</h3>
<p>Just paste your JSON on the left, and the Java code appears on the right. No clicking "Submit" or waiting for page reloads. It's reactive and instant.</p>
<h3 id="2-modern-java-support-records">2. Modern Java Support (Records!)</h3>
<p>Java 14+ introduced Records, which are perfect for DTOs. My tool supports them out of the box. You can toggle between standard Classes and Records with a single click.</p>
<h3 id="3-lombok--jackson-integration">3. Lombok &#x26; Jackson Integration</h3>
<p>Most of us use Lombok to avoid boilerplate and Jackson for JSON processing. You can configure the tool to automatically add:</p>
<ul>
<li><code>@Data</code> annotations (for Lombok)</li>
<li><code>@JsonProperty</code> annotations (for Jackson)</li>
</ul>
<p><img src="https://www.rabinarayanpatra.com/images/blogs/json-to-dto/config.webp" alt="Configuration Options" width="1600" height="1179"></p>
<h3 id="4-clean-ui">4. Clean UI</h3>
<p>I'm a believer that developer tools shouldn't look like they were built in 1999. I designed this with a dark mode "Electric Midnight" theme that's easy on the eyes during those late-night coding sessions.</p>
<h2 id="how-do-you-use-the-json-to-dto-converter">How do you use the JSON to DTO converter?</h2>
<ol>
<li>Go to <a href="https://json-to-dto.rabinarayanpatra.com/">json-to-dto.rabinarayanpatra.com</a>.</li>
<li>Paste your JSON payload.</li>
<li>Tweak the settings (Class name, Package name, toggle Records/Lombok).</li>
<li>Click <strong>Copy Code</strong>.</li>
<li>Paste it into your IDE. Done.</li>
</ol>
<p>I hope this saves you some time on your next project. Let me know if you have any feature requests!</p>
<p>For related tools and documentation, see the <a href="https://github.com/FasterXML/jackson-databind">Jackson Databind documentation</a>, <a href="https://openjdk.org/jeps/395">Java Records JEP 395</a>, and <a href="https://projectlombok.org/">Project Lombok</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">10 Essential Java Libraries Beyond Lombok</a> — The libraries that complement your generated DTOs, from MapStruct for mapping to Jackson for serialization.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/sanitizer-lib-intro">Sanitizer-Lib: Eliminating Input Sanitization Boilerplate</a> — Once you've generated your DTOs, annotate them with Sanitizer-Lib to handle input cleaning automatically.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Google Antigravity: Why I Think It Changes Everything]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/google-antigravity</link>
      <guid>https://www.rabinarayanpatra.com/blogs/google-antigravity</guid>
      <pubDate>Mon, 01 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[My personal take on Google Antigravity, the new agentic AI platform, and what it means for the future of our craft.]]></description>
      <content:encoded><![CDATA[<p>I've been thinking a lot lately about where we're heading as developers. For years, we've been the builders, the ones typing out the syntax, wrestling with semicolons, and debugging obscure errors. But with the launch of <strong>Google Antigravity</strong>, I feel the ground shifting beneath us.</p>
<p>It's not just another tool. It's a glimpse into a future where we stop being just "coders" and start being true "architects."</p>
<h2 id="how-does-google-antigravity-go-beyond-autocomplete">How does Google Antigravity go beyond autocomplete?</h2>
<p>We've all gotten used to AI assistants. They're great at finishing our sentences or writing a quick utility function. But Antigravity is different. It’s built on <strong>Gemini 3 Pro</strong>, and it introduces the concept of <strong>Agency</strong>.</p>
<p>When I look at this, I don't see a chatbot. I see a junior developer who never sleeps. You don't tell it <em>how</em> to write a loop; you tell it, "I need a user authentication system," and it plans, executes, and verifies the whole thing.</p>
<h2 id="what-is-the-antigravity-effect-on-developer-productivity">What is the "Antigravity" effect on developer productivity?</h2>
<p>At first, I'll admit, I felt a twinge of skepticism. If the AI does the heavy lifting, what happens to the joy of coding? The "flow state"?</p>
<p>But then I realized something. The "heavy lifting" is often just... gravity. It's the friction of setting up boilerplates, configuring webpack, or writing the same CRUD endpoints for the hundredth time. Antigravity removes that weight.</p>
<p>It frees me up to think about the <strong>product</strong>.</p>
<ul>
<li>How should this feature <em>feel</em> to the user?</li>
<li>Does this architecture scale?</li>
<li>What is the story we are telling with this data?</li>
</ul>
<p>That is where the real value lies. The AI handles the "how"; I focus on the "why."</p>
<h2 id="how-will-agentic-ai-change-the-developer-role">How will agentic AI change the developer role?</h2>
<p>I believe we are entering an era of <strong>Collaborative Intelligence</strong>.</p>
<p>In the near future, I won't be opening my IDE to write code from scratch. I'll be opening it to orchestrate a team of AI agents. One agent handles the database schema, another optimizes the frontend performance, and a third writes the integration tests.</p>
<p>My role—<strong>our role</strong>—will evolve. We will become:</p>
<ol>
<li><strong>Reviewers</strong>: Ensuring the AI's output aligns with our vision and security standards (thanks to Antigravity's "Artifacts" and transparency, this is actually feasible).</li>
<li><strong>System Designers</strong>: Connecting the dots between complex systems that are too large for a single human mind to hold at once.</li>
<li><strong>Creative Directors</strong>: Pushing the boundaries of what's possible, knowing we have the engine to build it.</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>Google Antigravity isn't about replacing us. It's about levelling us up. It's giving us the power to build things that were previously impossible for a single developer or a small team.</p>
<p>The gravity of mundane tasks is gone. The only limit now is our imagination.</p>
<p>Let's fly.</p>
<p>For more on the evolution of AI-assisted development, see the <a href="https://deepmind.google/research/">Google DeepMind Research</a>, <a href="https://www.anthropic.com/research">Anthropic's research on AI safety</a>, and <a href="https://github.blog/news-insights/research/">GitHub's report on AI in software development</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/agentic-payments-razorpay-npci-upi">Agentic Payments: What Razorpay and NPCI Just Pulled Off</a> — A concrete example of agentic AI moving from code generation into real-world financial transactions.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/my-thoughts-on-vibe-coding-2025">My Thoughts on Vibe Coding</a> — How AI-assisted development feels in practice, from the creative highs to the cleanup reality.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Java 25 Performance: How Compact Object Headers Save 20% Memory]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/java-25-compact-object-headers</link>
      <guid>https://www.rabinarayanpatra.com/blogs/java-25-compact-object-headers</guid>
      <pubDate>Mon, 01 Dec 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Let's talk about JEP 519. It's a game-changer in Java 25 that slashes object overhead by 50%, saving you a ton of heap memory.]]></description>
      <content:encoded><![CDATA[<h1 id="java-25-performance-how-compact-object-headers-save-20-memory">Java 25 Performance: How Compact Object Headers Save 20% Memory</h1>
<p>Let's be real for a second: Java objects have always been a little... heavy.</p>
<p>For as long as most of us have been coding, every object in the HotSpot VM has carried a hidden "tax"—the object header. On 64-bit systems, this header takes up anywhere from 96 to 128 bits (that's 12 to 16 bytes!) just to exist.</p>
<p>If you're creating millions of tiny objects—like <code>Integer</code>, <code>Optional</code>, or simple data carriers—you're often paying more for the packaging than the actual product.</p>
<p>But here's the good news: <strong>Java 25</strong> is finally fixing this with <strong>JEP 519: Compact Object Headers</strong>. It's a feature that shrinks that overhead down to a lean 64 bits (8 bytes), and honestly, it's going to be a massive win for modern applications.</p>
<h2 id="what-is-the-12-byte-object-header-tax-in-java">What is the 12-byte object header tax in Java?</h2>
<p>So, what exactly was taking up all that space? Before Java 25, an object header had two main roommates:</p>
<ol>
<li><strong>Mark Word (64 bits)</strong>: This holds the messy stuff like hash codes, GC age, and locking info.</li>
<li><strong>Class Word (64 bits)</strong>: A pointer telling the JVM, "Hey, I'm a String" or "I'm a HashMap."</li>
</ol>
<p>Even with optimizations like Compressed Oops, this usually rounded up to <strong>16 bytes</strong> because of memory alignment rules.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>Legacy Object Layout (16 bytes):</span></span>
<span data-line=""><span>+------------------+------------------+</span></span>
<span data-line=""><span>|    Mark Word     |   Class Word     |</span></span>
<span data-line=""><span>|    (8 bytes)     |    (4 bytes)     |</span></span>
<span data-line=""><span>+------------------+------------------+</span></span>
<span data-line=""><span>|     Padding      |      Fields      |</span></span>
<span data-line=""><span>|    (4 bytes)     |       ...        |</span></span>
<span data-line=""><span>+------------------+------------------+</span></span></code></pre></figure>
<p>Think about it: if you have 10 million <code>Integer</code> objects in memory, you aren't just storing numbers. You're storing <strong>160MB of headers</strong>. That adds up fast.</p>
<h2 id="how-does-jep-519-reduce-object-header-size">How does JEP 519 reduce object header size?</h2>
<p>The Java team basically pulled a "compression" magic trick with JEP 519. They asked, "Do we really need a separate field just to point to the class?"</p>
<p>The answer? <strong>Nope.</strong></p>
<p>They figured out how to squash that class pointer <em>inside</em> the Mark Word itself. It's some serious bit-packing wizardry, but the result is a sleek, single 64-bit header.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>Compact Object Header (8 bytes):</span></span>
<span data-line=""><span>+---------------------------------------------------------------+</span></span>
<span data-line=""><span>|                        Mark Word (64 bits)                    |</span></span>
<span data-line=""><span>+---------------------------------------------------------------+</span></span>
<span data-line=""><span>| 22 bits: Compressed Class ID | 31 bits: Hash | 4 bits: GC Age | ...</span></span>
<span data-line=""><span>+---------------------------------------------------------------+</span></span></code></pre></figure>
<ul>
<li><strong>Compressed Class Pointer</strong>: Instead of a full address, it uses a 22-bit ID. This supports up to ~4 million classes, which is plenty for 99.9% of us.</li>
<li><strong>Everything Else</strong>: Hash codes and GC info are still there, safe and sound.</li>
</ul>
<h2 id="how-much-memory-can-compact-object-headers-save">How much memory can compact object headers save?</h2>
<p>Okay, cool tech, but what does it mean for your app?</p>
<p>It means <strong>free memory</strong>. By shaving 8 bytes off <em>every single object</em>, your heap usage drops like a rock.</p>
<ul>
<li><strong>Small Objects</strong>: An <code>Integer</code> goes from 16 bytes to 8 bytes. That's a <strong>50% reduction</strong> in overhead.</li>
<li><strong>Real-World Results</strong>: Amazon ran this on thousands of production services and saw an average <strong>heap reduction of 20%</strong>.</li>
</ul>
<blockquote>
<p>[!IMPORTANT]
Less memory usage isn't just about saving RAM. It makes your Garbage Collector's life way easier. Less data to scan means faster GC cycles and less latency for your users.</p>
</blockquote>
<h2 id="how-do-you-enable-compact-object-headers-in-java-25">How do you enable compact object headers in Java 25?</h2>
<p>In Java 25, this feature is ready for prime time, but you have to ask for it (for now). Just add this flag:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -XX:+UseCompactObjectHeaders</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -jar</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> my-app.jar</span></span></code></pre></figure>
<p><em>Quick note: If you were playing with this in Java 24, you needed the <code>UnlockExperimentalVMOptions</code> flag. You can drop that now—it's graduated!</em></p>
<h2 id="what-are-the-limitations-of-compact-object-headers">What are the limitations of compact object headers?</h2>
<p>While this is safe for almost everyone, there are two tiny edge cases:</p>
<ol>
<li><strong>The "Mega-Monolith"</strong>: If your app loads more than 4 million unique classes (which is... a lot), the compressed ID might run out of space.</li>
<li><strong>Heavy Locking</strong>: If you're doing a ton of synchronization on millions of objects at once, the JVM has a bit less room to track that, so it might need to do some extra work.</li>
</ol>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Compact Object Headers is one of those rare "free lunches" in software engineering. You don't have to rewrite your code, you don't have to refactor your architecture, and you get double-digit memory savings.</p>
<p>If you're upgrading to Java 25, flipping this switch should be the first thing you do. Your servers will thank you!</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://openjdk.org/jeps/519">JEP 519: Compact Object Headers</a> — The official JDK Enhancement Proposal</li>
<li><a href="https://openjdk.org/projects/code-tools/jol/">JOL (Java Object Layout) Tool</a> — Tool for analyzing Java object memory layout</li>
<li><a href="https://wiki.openjdk.org/display/lilliput">OpenJDK Project Lilliput</a> — The umbrella project for object header reduction</li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Mastering Virtual Threads in Java 25</a> — The other headline feature in Java 25 that transforms concurrency performance.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-interview-2025">Top Java Interview Questions for 2025</a> — Compact Object Headers and JVM internals are increasingly popular interview topics.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">10 Essential Java Libraries Beyond Lombok</a> — More tools to reduce boilerplate and boost productivity alongside JVM-level improvements.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[The One System Design Question That Failed 80% of Candidates in 2025]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/system-design-question-failed-candidates-2025</link>
      <guid>https://www.rabinarayanpatra.com/blogs/system-design-question-failed-candidates-2025</guid>
      <pubDate>Fri, 28 Nov 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[It sounded simple: "Design a Rate Limiter." But there was a twist that caught almost everyone off guard.]]></description>
      <content:encoded><![CDATA[<p>I've been interviewing backend engineers for the last few months, and I noticed a pattern. A painful one.</p>
<p>I ask a question that seems standard—almost boring—and watch as 8 out of 10 smart, capable developers walk right into a trap.</p>
<p>The question isn't a riddle. It's not "invert a binary tree on a whiteboard." It's a real-world problem we faced last month.</p>
<p>Here it is:</p>
<blockquote>
<p><strong>"Design a Rate Limiter for our new AI Agent API."</strong></p>
</blockquote>
<p>"Easy," they say. "I'll use Redis. Token Bucket algorithm. 100 requests per minute. Next question?"</p>
<p>And that's where they fail.</p>
<h2 id="what-ai-specific-twist-do-interviewers-add-to-rate-limiter-questions">What AI-specific twist do interviewers add to rate limiter questions?</h2>
<p>If this were a standard REST API where requests take 50ms, the Token Bucket answer would be perfect. But this is an <strong>AI Agent API</strong>.</p>
<p>Here's the context I give them next:</p>
<ol>
<li><strong>Requests are slow.</strong> Generating a response can take 30-60 seconds.</li>
<li><strong>Cost is variable.</strong> One request might use 10 tokens (cheap); another might use 10,000 tokens (expensive).</li>
<li><strong>Concurrency matters.</strong> If a user sends 100 requests instantly, and each takes 60 seconds, you're holding 100 open connections. Your server memory will explode before you even hit the "rate limit."</li>
</ol>
<h2 id="what-trap-catches-80-of-candidates-in-rate-limiter-design">What trap catches 80% of candidates in rate limiter design?</h2>
<p>Most candidates optimize for <strong>throughput</strong> (requests per second).
But for LLM apps, you need to optimize for <strong>concurrency</strong> (active requests) and <strong>cost</strong> (token usage).</p>
<p>If you just limit "10 requests per minute," a user could send 10 massive prompts simultaneously, lock up your worker threads for a full minute, and cost you $5 in a single burst.</p>
<h2 id="how-do-you-correctly-design-a-distributed-rate-limiter-for-ai-workloads">How do you correctly design a distributed rate limiter for AI workloads?</h2>
<p>The candidates who passed didn't just throw "Redis" at the problem. They asked about the <em>workload</em>.</p>
<p>Here's the simple, robust solution we were looking for:</p>
<h3 id="1-the-waiting-room-leaky-bucket">1. The "Waiting Room" (Leaky Bucket)</h3>
<p>First, we need to protect our servers from exploding. We use a <strong>Leaky Bucket</strong> queue.</p>
<ul>
<li>Requests enter a queue (Redis List or SQS).</li>
<li>Workers pull requests at a fixed rate (e.g., 5 concurrent jobs max).</li>
<li>If the queue is full, we reject immediately (429 Too Many Requests).</li>
</ul>
<h3 id="2-the-wallet-token-bucket">2. The "Wallet" (Token Bucket)</h3>
<p>Second, we need to protect our bank account.</p>
<ul>
<li>We don't limit <em>requests</em>. We limit <em>compute units</em>.</li>
<li>Each user has a "wallet" of points per minute.</li>
<li>Before processing, we estimate the cost. If they have enough points, we proceed.</li>
</ul>
<h2 id="why-is-rate-limiter-design-critical-for-ai-era-system-design-interviews">Why is rate limiter design critical for AI-era system design interviews?</h2>
<p>The reason this question trips people up isn't because they don't know System Design. It's because they're on autopilot.</p>
<p>They hear "Rate Limiter" and their brain auto-completes to "Redis Token Bucket."</p>
<p>In 2025, the best engineers aren't the ones who memorized the "Cracking the Coding Interview" book. They're the ones who pause, look at the specific constraints of <em>this</em> problem, and realize that an AI Agent is a very different beast than a CRUD app.</p>
<p><strong>Takeaway:</strong> Next time you're in an interview, don't rush to the solution. Fall in love with the problem first. Ask about the latency. Ask about the cost.</p>
<p>That's how you pass the 80% fail rate.</p>
<h2 id="what-does-a-complete-rate-limiter-architecture-look-like">What does a complete rate limiter architecture look like?</h2>
<p>Let me draw the full picture that the top 20% of candidates described:</p>
<h3 id="the-architecture">The Architecture</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="text" data-theme="material-theme github-light"><code data-language="text" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span>                        ┌────────────────┐</span></span>
<span data-line=""><span>    Client Request ───▶ │   API Gateway  │</span></span>
<span data-line=""><span>                        │  (Cloudflare)  │</span></span>
<span data-line=""><span>                        └───────┬────────┘</span></span>
<span data-line=""><span>                                │</span></span>
<span data-line=""><span>                        ┌───────▼────────┐</span></span>
<span data-line=""><span>                        │  Rate Limiter  │</span></span>
<span data-line=""><span>                        │   (Redis)      │</span></span>
<span data-line=""><span>                        └───────┬────────┘</span></span>
<span data-line=""><span>                                │</span></span>
<span data-line=""><span>                  ┌─────────────┼─────────────┐</span></span>
<span data-line=""><span>                  ▼             ▼              ▼</span></span>
<span data-line=""><span>           ┌──────────┐ ┌──────────┐  ┌──────────┐</span></span>
<span data-line=""><span>           │ Waiting   │ │  Wallet  │  │ Circuit  │</span></span>
<span data-line=""><span>           │  Room     │ │ (Token   │  │ Breaker  │</span></span>
<span data-line=""><span>           │ (Queue)   │ │  Budget) │  │          │</span></span>
<span data-line=""><span>           └──────────┘ └──────────┘  └──────────┘</span></span></code></pre></figure>
<h3 id="layer-1-the-waiting-room-concurrency-limiter">Layer 1: The Waiting Room (Concurrency Limiter)</h3>
<p>This is a <strong>Leaky Bucket</strong> or <strong>Semaphore</strong> that protects your servers from being overwhelmed.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ConcurrencyLimiter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> RSemaphore</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> semaphore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ConcurrencyLimiter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">RedissonClient</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> redisson</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">semaphore </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> redisson</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getSemaphore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ai-api:concurrency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">semaphore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">trySetPermits</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">50</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Max 50 concurrent AI jobs</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> T</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> executeWithLimit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Supplier</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> task</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">semaphore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">tryAcquire</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TimeUnit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SECONDS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> TooManyRequestsException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Queue full. Try again later.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> task</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> finally</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            semaphore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">release</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="layer-2-the-wallet-cost-based-token-bucket">Layer 2: The Wallet (Cost-Based Token Bucket)</h3>
<p>Instead of counting requests, count <strong>compute units</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TokenBudgetLimiter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> RedisTemplate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> redis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> tryConsume</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> estimatedTokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> key </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">budget:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Each user gets 100,000 tokens per minute</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> remaining </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> redis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">opsForValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">decrement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> estimatedTokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">remaining </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ||</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> remaining </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // Refund and reject</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            redis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">opsForValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> estimatedTokens</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Reset budgets every minute via scheduled task</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Scheduled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">fixedRate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 60_000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> resetBudgets</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Reset all user budgets to 100,000</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="layer-3-the-circuit-breaker">Layer 3: The Circuit Breaker</h3>
<p>When the upstream AI model (OpenAI, Claude, etc.) is degraded, stop sending new requests.</p>
<p>This prevents cascading failures. If the AI provider's latency spikes from 30 seconds to 120 seconds, your "Waiting Room" fills up fast. The circuit breaker detects this and starts rejecting requests immediately instead of queueing them.</p>
<h2 id="what-other-rate-limiting-algorithms-should-you-know">What other rate limiting algorithms should you know?</h2>
<p>For completeness, here are the four algorithms you should be ready to discuss:</p>
<table>
<thead>
<tr>
<th>Algorithm</th>
<th>Best For</th>
<th>Weakness</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Token Bucket</strong></td>
<td>Bursty traffic, API quotas</td>
<td>Doesn't limit concurrency</td>
</tr>
<tr>
<td><strong>Leaky Bucket</strong></td>
<td>Smoothing traffic, queuing</td>
<td>Fixed rate, can't handle bursts</td>
</tr>
<tr>
<td><strong>Fixed Window</strong></td>
<td>Simple rate counting</td>
<td>Boundary burst problem</td>
</tr>
<tr>
<td><strong>Sliding Window Log</strong></td>
<td>Precise rate limiting</td>
<td>Memory-intensive at scale</td>
</tr>
</tbody>
</table>
<p>The winning answer for AI workloads combines <strong>Leaky Bucket</strong> (concurrency) with <strong>Token Bucket</strong> (cost). Neither alone is sufficient.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://stripe.com/blog/rate-limiters">Stripe Engineering: Rate Limiting</a> — Practical rate limiting strategies from Stripe</li>
<li><a href="https://redis.io/docs/latest/develop/use/patterns/rate-limiting/">Redis Rate Limiting Documentation</a> — Redis-based rate limiting patterns</li>
<li><a href="https://developers.cloudflare.com/waf/rate-limiting-rules/">Cloudflare Rate Limiting</a> — How Cloudflare implements rate limiting at scale</li>
<li><a href="https://www.amazon.com/System-Design-Interview-insiders-Second/dp/B08CMF2CQF">System Design Interview by Alex Xu</a> — Chapter 4 covers rate limiter design in depth</li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">Implementing the Outbox Pattern with CDC in Microservices</a> — Another system design pattern that trips up candidates: reliable event delivery across services.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-i-turned-daily-problem-solving-into-a-dsa-habit">How I Turned Daily Problem Solving into a DSA Habit</a> — System design interviews test breadth, but DSA rounds test depth; here's how to stay sharp on both.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-interview-2025">Top Java Interview Questions for 2025</a> — Pair your system design prep with strong language fundamentals for a well-rounded interview performance.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Building a Modern Documentation Generator with Next.js 16]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16</link>
      <guid>https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16</guid>
      <pubDate>Wed, 19 Nov 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[I got tired of setting up docs for every project, so I built a documentation generator that actually makes it fun. Zero config, MDX support, dark mode, search—the whole deal.]]></description>
      <content:encoded><![CDATA[<p>You know what nobody talks about? How annoying documentation setup is.</p>
<p>Not the writing part—I actually like writing docs. What I hate is the hour-long yak shave before you can write a single word. Installing doc frameworks, configuring themes, setting up search, making sure it looks decent on mobile... and you haven't even started documenting your actual project yet.</p>
<p>I kept hitting this wall. Every new project, same dance. "Oh, I should document this." Then I'd spend a weekend comparing Docusaurus vs MkDocs vs VuePress, reading setup guides, tweaking config files. By Sunday evening, I'd have a hello-world docs site and zero actual documentation.</p>
<p>There had to be a better way.</p>
<h2 id="what-features-does-a-good-documentation-generator-need">What features does a good documentation generator need?</h2>
<p>I made a mental list every time I set up docs. It went like this:</p>
<p><strong>For me:</strong></p>
<ul>
<li>Fork a repo, add my Markdown files, done</li>
<li>No database. No complex config files. Just files.</li>
<li>TypeScript because I don't trust myself</li>
<li>Fast dev server (waiting for rebuilds kills flow)</li>
</ul>
<p><strong>For users:</strong></p>
<ul>
<li>Fast. Like, stupid fast.</li>
<li>Works on phones (people debug at 2 AM on their phone)</li>
<li>Dark mode (it's 2025, come on)</li>
<li>Search that actually works</li>
<li>Code blocks that don't look like trash</li>
</ul>
<p><strong>Nice-to-haves:</strong></p>
<ul>
<li>Diagrams (Mermaid)</li>
<li>Math equations (KaTeX)</li>
<li>Custom components (sometimes Markdown isn't enough)</li>
</ul>
<h2 id="how-is-the-documentation-generator-built-with-nextjs-16">How is the documentation generator built with Next.js 16?</h2>
<p>I went with Next.js 16. Mostly because I know it well, but also because Turbopack is genuinely fast. And server components make static generation weirdly simple.</p>
<p>The core idea was stupid simple: drop Markdown files in a <code>content</code> folder, they become pages. That's it. No routing config. No page registry. Just files.</p>
<pre><code>content/
├── getting-started/
│   ├── index.md
│   └── installation.md
├── guides/
│   └── writing-docs.md
└── api/
    └── overview.md
</code></pre>
<p>Those files become <code>/getting-started</code>, <code>/getting-started/installation</code>, <code>/guides/writing-docs</code>, <code>/api/overview</code>. Obvious. Predictable. Zero config.</p>
<h3 id="what-features-make-this-docs-generator-stand-out">What features make this docs generator stand out?</h3>
<p><strong>Search without a backend.</strong> I used FlexSearch—runs entirely in the browser. Index builds at compile time, search is instant. No API calls, no loading states. Just type and see results.</p>
<p><strong>MDX for everything.</strong> Sometimes you need more than text. With MDX, you can use React components inline. Need a warning callout? Custom tabs? Just drop in a component:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="mdx" data-theme="material-theme github-light"><code data-language="mdx" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Callout</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> type</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">warning</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C792EA;--shiki-light:#6F42C1"> title</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Heads Up</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  This will delete everything. Yes, everything.</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">Callout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Clean, readable, and it renders as an actual styled warning box. Way better than blockquote hacks.</p>
<p><strong>Code blocks that don't suck.</strong> I used Shiki because it uses the same highlighting engine as VS Code. Your code looks exactly like it does in your editor. Plus I added:</p>
<ul>
<li>Copy button (because typing examples is tedious)</li>
<li>Line highlighting (to draw attention)</li>
<li>Filename display (for context)</li>
</ul>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="typescript" data-theme="material-theme github-light"><code data-language="typescript" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> calculateTotal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Item</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // This line is highlighted</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> subtotal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> items</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">reduce</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit">sum</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> sum</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">price</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#F07178;--shiki-light:#24292E">)</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // These lines too</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> tax</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> subtotal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0.1</span></span>
<span data-line="" data-highlighted-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#005CC5"> total</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> subtotal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tax</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">  return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> total</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>See those highlighted lines? Built-in. Just add <code>{2,4-6}</code> to your code fence.</p>
<p><strong>Dark mode that actually works.</strong> Used next-themes. Respects system preference. No flash of wrong theme. Everything adapts—even Mermaid diagrams and code blocks. Just works.</p>
<h3 id="how-does-it-handle-seo-performance-and-type-safety">How does it handle SEO, performance, and type safety?</h3>
<p><strong>SEO:</strong> Auto sitemap, RSS feed, Open Graph tags, structured data. All the stuff that makes Google happy. You write frontmatter, it handles the rest.</p>
<p><strong>Performance:</strong> Everything's static. No database queries. Turbopack makes builds fast. Whole site is just HTML, CSS, JS. Simple. Fast. Works everywhere.</p>
<p><strong>TypeScript:</strong> Caught so many bugs. Frontmatter validation, document types, navigation types. If it compiles, it probably works. Worth the occasional type gymnastics.</p>
<h2 id="what-did-i-learn-building-this-documentation-tool">What did I learn building this documentation tool?</h2>
<p><strong>Defaults > options.</strong> Every config option is a decision. Good defaults mean people can skip most decisions and just write.</p>
<p><strong>Speed is a feature.</strong> When dev is instant and builds are fast, you actually want to preview changes. Slow tools make you avoid iterating.</p>
<p><strong>Show, don't tell.</strong> This docs site? It documents itself. Every feature is demonstrated in the docs. If something doesn't work well enough to document itself, it's not good enough.</p>
<h2 id="what-were-the-messy-parts-of-the-first-version">What were the messy parts of the first version?</h2>
<p>I'm not gonna lie—the first version was chaos. I had:</p>
<ul>
<li>Hardcoded paths everywhere</li>
<li>No error handling</li>
<li>Mixed UI with logic</li>
<li>Comments like "TODO: fix this properly"</li>
</ul>
<p>Classic vibe coding. But I went back and cleaned it up. Pulled out utilities. Added proper types. Wrote tests. Made it something I'd be okay with other people using.</p>
<p>That's the thing about side projects—you can ship the messy version fast, but you gotta go back and polish it if you want people to actually use it.</p>
<h2 id="what-features-are-planned-next">What features are planned next?</h2>
<p>I'm using this for my own projects now. Already found some rough edges, smoothed them out. Added features I realized I needed.</p>
<p>Maybe I'll add:</p>
<ul>
<li>Version switching (for versioned APIs)</li>
<li>Multi-language support (i18n)</li>
<li>Comments widget</li>
<li>Analytics dashboard</li>
</ul>
<p>But honestly? It does what I need. Fork it, add content, deploy. Documentation in minutes, not hours.</p>
<h2 id="how-can-you-try-it-yourself">How can you try it yourself?</h2>
<p>If you're tired of complex doc setups, give it a shot:</p>
<ol>
<li>Fork <a href="https://github.com/rabinarayanpatra/docs-generator">github.com/rabinarayanpatra/docs-generator</a></li>
<li><code>npm install</code></li>
<li>Add Markdown files to <code>content/</code></li>
<li><code>npm run dev</code></li>
</ol>
<p>Four steps. That's it.</p>
<p>And hey, if you find bugs or have ideas, PRs welcome. I built this because I needed it, but I'd love to see what you do with it.</p>
<hr>
<p><strong>Tech stuff:</strong> Next.js 16, React 19, TypeScript, Tailwind CSS, MDX, FlexSearch, Shiki, Mermaid, KaTeX. Full list in the README.</p>
<p><strong>The real story:</strong> I was procrastinating on documenting a different project, so I built this instead. Classic developer move.</p>
<p>For the tools used in this project, see the <a href="https://nextjs.org/docs">Next.js 16 documentation</a>, <a href="https://mdxjs.com/docs/">MDX documentation</a>, <a href="https://github.com/nicedoc/flexsearch">FlexSearch</a>, <a href="https://shiki.style/">Shiki syntax highlighter</a>, and <a href="https://mermaid.js.org/">Mermaid diagramming</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/hello-proxy-ts-nextjs-16">Goodbye middleware.ts, Hello proxy.ts: The Next.js 16 Migration Guide</a> — The proxy.ts change in Next.js 16 that affects every Next.js project, including this docs generator.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/my-thoughts-on-vibe-coding-2025">My Thoughts on Vibe Coding</a> — The creative coding mindset that led to building tools like this docs generator.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Why `1 == 1` is True but `128 == 128` is False in Java — The Integer Caching Trap Explained]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/why-1-1-is-true-but-128-128-is-false-in-java</link>
      <guid>https://www.rabinarayanpatra.com/blogs/why-1-1-is-true-but-128-128-is-false-in-java</guid>
      <pubDate>Sun, 20 Jul 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Discover the surprising behavior behind Java's Integer caching mechanism and why `==` comparisons can lead to unexpected results.]]></description>
      <content:encoded><![CDATA[<hr>
<p>Ever written a simple Java comparison like <code>Integer a = 128; Integer b = 128;</code> and got surprised that <code>a == b</code> returned <code>false</code>? 🤯 You're not alone.</p>
<p>Let’s unravel this fascinating corner of the Java language where <strong>autoboxing</strong>, <strong>object pooling</strong>, and <strong>reference comparison</strong> collide in unexpected ways.</p>
<h2 id="why-does-java-produce-different-results-for-1--1-and-128--128">Why does Java produce different results for <code>1 == 1</code> and <code>128 == 128</code>?</h2>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> a </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> b </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">a </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> b</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // false</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> x </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> y </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">x </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> y</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // true</span></span></code></pre></figure>
<p>At first glance, both <code>a</code> and <code>b</code> are <code>Integer</code> objects with the same value. So why does <code>a == b</code> return <code>false</code>, while <code>x == y</code> returns <code>true</code>?</p>
<h3 id="short-answer">Short Answer:</h3>
<p>Java <strong>caches Integer objects</strong> from <strong>-128 to 127</strong>. Outside that range, new objects are created — even if the values are the same.</p>
<h2 id="how-does-javas-integer-caching-mechanism-work">How does Java's Integer caching mechanism work?</h2>
<p>In Java, <code>Integer</code> is a <strong>wrapper class</strong>, and assigning <code>int</code> to <code>Integer</code> uses <strong>autoboxing</strong>. When you do:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> x </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Java checks if that number is in the cache. If it is, it reuses the object. This makes:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> x </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> y </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">x </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> y</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // true ✅</span></span></code></pre></figure>
<p>Because both point to the <strong>same object in memory</strong>.</p>
<p>But if the value is outside <code>-128 to 127</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> a </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> b </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">a </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> b</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // false ❌</span></span></code></pre></figure>
<p>New <code>Integer</code> objects are created each time, so <code>a</code> and <code>b</code> point to different memory locations.</p>
<h2 id="how-should-you-compare-integer-objects-in-java">How should you compare Integer objects in Java?</h2>
<p>Unlike <code>==</code> which checks <strong>reference equality</strong>, <code>.equals()</code> compares the <strong>actual values</strong>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> a </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> b </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">a</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">b</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // true ✅</span></span></code></pre></figure>
<p>Always use <code>.equals()</code> when comparing wrapper objects or values from unknown sources.</p>
<h2 id="can-you-customize-the-integer-cache-range-in-java">Can you customize the Integer cache range in Java?</h2>
<p>You can tweak the cache range by setting a JVM option:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">-XX:AutoBoxCacheMax</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">=256</span></span></code></pre></figure>
<p>This increases the upper bound of cached values. Note: this works only for <code>Integer</code>, not other wrappers.</p>
<h2 id="-memory-check-example-with-systemidentityhashcode">🔬 Memory Check Example with <code>System.identityHashCode()</code></h2>
<p>To prove that cached and non-cached Integers are different objects:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> c </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> d </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 128</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">identityHashCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">c</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">identityHashCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">d</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Integer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> f </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">identityHashCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">identityHashCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">f</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span></code></pre></figure>
<p>You'll observe that:</p>
<ul>
<li><code>c</code> and <code>d</code> have <strong>different hash codes</strong> (not cached)</li>
<li><code>e</code> and <code>f</code> have <strong>same hash code</strong> (cached)</li>
</ul>
<h2 id="why-does-integer-caching-matter-in-real-world-java-projects">Why does Integer caching matter in real-world Java projects?</h2>
<p>This behavior can cause hard-to-find bugs when developers rely on <code>==</code> for wrapper classes. Some common issues:</p>
<ul>
<li>Comparing response values from APIs or databases</li>
<li>Conditional logic failures in object comparisons</li>
<li>Test assertions failing unexpectedly</li>
</ul>
<h2 id="when-should-you-use-equals-instead-of--in-java">When should you use <code>.equals()</code> instead of <code>==</code> in Java?</h2>
<p>Use <code>.equals()</code> when comparing:</p>
<ul>
<li>Boxed types (<code>Integer</code>, <code>Long</code>, etc.)</li>
<li>Objects from user input or external systems</li>
</ul>
<p>Use <code>==</code> only when:</p>
<ul>
<li>Comparing primitives (<code>int</code>, <code>long</code>, etc.)</li>
<li>Checking for <strong>exact same object instance</strong> (rare)</li>
</ul>
<h2 id="what-should-every-java-developer-know-about-integer-caching">What should every Java developer know about Integer caching?</h2>
<ul>
<li>Java caches Integer values from <code>-128</code> to <code>127</code>.</li>
<li><code>==</code> compares references, not values.</li>
<li><code>.equals()</code> is the correct way to compare object values.</li>
<li>Autoboxing can introduce subtle bugs if not understood well.</li>
</ul>
<hr>
<p><strong>Did this save you from a bug?</strong> Hit that bookmark or share it with your Java buddies!</p>
<p>Want more Java mysteries decoded? <a href="https://github.com/rabinarayanpatra">Follow me on GitHub</a> or <a href="https://instagram.com/rabinarayan01">connect on Instagram</a>. ☕</p>
<p>For the official specification, see the <a href="https://docs.oracle.com/javase/specs/jls/se21/html/jls-5.html#jls-5.1.7">Java Language Specification §5.1.7 on Boxing Conversion</a> and the <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Integer.html#valueOf(int)">Integer.valueOf() Javadoc</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-interview-2025">Top Java Interview Questions for 2025</a> — This kind of JVM trivia is exactly what interviewers love to ask about.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-25-compact-object-headers">Java 25 Compact Object Headers</a> — Dive deeper into how Java represents objects in memory at the JVM level.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[My Thoughts on Vibe Coding]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/my-thoughts-on-vibe-coding-2025</link>
      <guid>https://www.rabinarayanpatra.com/blogs/my-thoughts-on-vibe-coding-2025</guid>
      <pubDate>Fri, 18 Jul 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[A personal take on vibe coding—when it works, when it hurts, and how I used it on rabinarayanpatra.com to add dark mode and a blog audio feature.]]></description>
      <content:encoded><![CDATA[<p>Last week I vibe-coded a dark-mode toggle on <a href="https://www.rabinarayanpatra.com">rabinarayanpatra.com</a> just because it felt right. No ticket, no plan—just me thinking, <em>“Eh, this site needs a dark mode.”</em> Thirty minutes later, it was live. It wasn’t polished, but it worked. And I liked it. It felt like sketching in code.</p>
<p>That, to me, is vibe coding.</p>
<h2 id="what-exactly-is-vibe-coding">What exactly is vibe coding?</h2>
<p>It’s not some formal technique. You won’t find it in an engineering handbook. Vibe coding is when you're just building by feel. Not for a deadline. Not because of some stakeholder. You're just trying something out. Maybe you're exploring a new stack or randomly experimenting at midnight. No roadmap—just intuition.</p>
<p>And sometimes? It’s the best kind of dev work.</p>
<h2 id="when-does-vibe-coding-actually-work-well">When does vibe coding actually work well?</h2>
<p>I’ve used vibe coding to prototype all sorts of stuff—especially on my personal site. One recent example: I added a “Listen to this blog” feature so visitors can hear posts read out loud. That wasn’t planned. I just thought it’d be cool. Started digging into text-to-speech options, wired it up in a quick session, and bam—it worked. A few tweaks later, it actually felt useful. But the first version? Total chaos under the hood.</p>
<p>Same with hackathons or when I’m exploring new frameworks. I’ve built entire layouts and animations in a single burst, just to see what happens. No pressure. No gatekeeping. Just momentum. That’s what makes vibe coding great—it gets you into flow. You’re not overthinking every line.</p>
<h2 id="what-are-the-risks-of-vibe-coding">What are the risks of vibe coding?</h2>
<p>But vibe coding does have a dark side.</p>
<p>It’s easy to leave behind a mess. When I coded that “listen” feature, I hardcoded strings, ignored loading states, and mixed UI with logic. At first, it was fine. But later, I had to dig through that mess to add pause/resume and fix some edge cases. I’ve learned: what feels fast now often costs double later.</p>
<p>The worst part? It <em>feels</em> done, even when it’s not. That’s the trap.</p>
<h2 id="how-do-you-contain-vibe-coding-and-make-it-production-ready">How do you contain vibe coding and make it production-ready?</h2>
<p>So I’ve made peace with vibe coding <em>by containing it</em>. Here’s what I do now:</p>
<ul>
<li>I label it mentally as “just sketching”—so I don’t expect the first version to be clean.</li>
<li>After I get something working, I slow down and <strong>review every line</strong>.</li>
<li>I pull out all the hacks: replace temp variables, fix structure, add comments.</li>
<li>I never merge vibe code straight into main. I refactor it, test it, and then treat it like “real” code.</li>
</ul>
<p>When I built the dark mode toggle, it started as a simple boolean switch jammed into a layout file. But after the basic toggle worked, I paused. I pulled it into a proper theme context, added local storage syncing, handled system preferences—<em>then</em> shipped it. Vibe coding got it started, but cleanup made it production-ready.</p>
<h2 id="what-rules-should-you-follow-when-vibe-coding">What rules should you follow when vibe coding?</h2>
<p>So here’s my no-fluff reminder:</p>
<ul>
<li>Don’t skip tests—just write <em>something</em>, even one.</li>
<li>Never commit unreviewed vibe code.</li>
<li>Clean it like someone else will read it tomorrow—because future-you <em>is</em> someone else.</li>
<li>Sketch fast, but polish slow.</li>
</ul>
<p>At the end of the day, vibe coding is why I still love this job. It’s creative. It’s weird. It’s fun. Some of my best ideas have come from just messing around on my own site, like late-night tinkering on <a href="https://www.rabinarayanpatra.com">rabinarayanpatra.com</a>. It’s like jazz—improv with a keyboard.</p>
<p>Anyway, I want to hear your story. What’s something <em>you</em> vibe-coded for fun? Got a weird little side feature, or a cool “just built it to see” moment? Share it—I’d love to see what you’re building when no one’s looking.</p>
<p>Let’s keep vibing.</p>
<p>For more on the psychology of developer flow states and creative coding, see Andrej Karpathy’s original <a href="https://x.com/karpathy/status/1886192184808149383">vibe coding post</a> that popularized the term, and Cal Newport’s <a href="https://calnewport.com/deep-work-rules-for-focused-success-in-a-distracted-world/">Deep Work</a> on sustained focus in creative work.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/building-modern-docs-generator-nextjs-16">Building a Modern Documentation Generator with Next.js 16</a> — A project born from vibe coding energy, turned into a polished open-source tool.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-i-turned-daily-problem-solving-into-a-dsa-habit">How I Turned Daily Problem Solving into a DSA Habit</a> — The disciplined counterpart to vibe coding: building consistency through daily practice.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/google-antigravity">Google Antigravity: Why I Think It Changes Everything</a> — How AI tools are amplifying the creative coding impulse that vibe coding taps into.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[How I Turned Daily Problem Solving into a DSA Habit (and Stuck with It)]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/how-i-turned-daily-problem-solving-into-a-dsa-habit</link>
      <guid>https://www.rabinarayanpatra.com/blogs/how-i-turned-daily-problem-solving-into-a-dsa-habit</guid>
      <pubDate>Sun, 06 Jul 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[A first-person, friendly walkthrough of how I built a consistent daily DSA problem-solving habit, the routines I use to stay motivated, and the systems that keep me improving without burning out.]]></description>
      <content:encoded><![CDATA[<p>If you had told me two years ago that I’d wake up excited to wrestle with a graph problem before breakfast, I would have laughed. Back then, data structures and algorithms were just items on an interview checklist. I’d binge-watch a playlist, grind for a weekend, and then forget half of it by Monday. Things only started to click when I stopped chasing the perfect crash course and treated DSA like brushing my teeth—quick, daily, and non-negotiable.</p>
<h2 id="how-does-a-coding-streak-build-lasting-dsa-habits">How does a coding streak build lasting DSA habits?</h2>
<p>The breakthrough came in the most ordinary way. I was venting to a friend about yet another failed attempt at a “30 days to FAANG” plan. He opened his notebook and showed me a simple log: two problems a day, every day, for eight months. No fireworks, just a neat column of dates and ticks. That quiet streak humbled me more than any viral roadmap.</p>
<p>So I copied his idea, but with a modest target: one problem a day for 30 days. No skipping weekends, no marathon catch-up sessions. I finished the month with 35 logged problems, a handful of messy notes, and a brain that instinctively reached for patterns like sliding window or two pointers. More importantly, the daily win rewired how I approached debugging, scoping complexity, and even design conversations at work.</p>
<h2 id="what-daily-dsa-routine-actually-sticks-long-term">What daily DSA routine actually sticks long-term?</h2>
<p>I didn’t stumble into consistency; I built guardrails for it.</p>
<h3 id="a-journal-that-keeps-me-honest">A Journal That Keeps Me Honest</h3>
<p>I keep a tiny “DSA Daily” template in Notion. Each entry records the problem link, how long I wrestled with it, the main idea, and the bug that almost tripped me. I also add a gut-feel confidence score out of five. It takes five minutes, but the act of writing forces me to explain the solution to myself instead of just celebrating the green check.</p>
<p>If you like templates, here’s the one I use:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="markdown" data-theme="material-theme github-light"><code data-language="markdown" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold">## </span><span style="--shiki-dark:#FFCB6B;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold">Problem</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Link:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Difficulty:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Topic:</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold">## </span><span style="--shiki-dark:#FFCB6B;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold">Approach</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Why it works:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Time complexity:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Space complexity:</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold">## </span><span style="--shiki-dark:#FFCB6B;--shiki-dark-font-weight:inherit;--shiki-light:#005CC5;--shiki-light-font-weight:bold">Notes</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Got stuck at:</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#E36209">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Next time I'll:</span></span></code></pre></figure>
<p>It’s nothing fancy, but the notes have become my own little textbook.</p>
<h3 id="matching-dsa-to-my-energy-curve">Matching DSA to My Energy Curve</h3>
<p>Morning me is curious; evening me just wants dinner. So I guard a 45-minute slot right after coffee for fresh problems. High-energy mornings get medium questions. Sleepy days mean an easier one or a redo of something that beat me last week. If life explodes, I lean on a “micro fallback”: reading an editorial and summarizing it still counts as a rep. That safety net keeps the streak alive without turning the habit into self-punishment.</p>
<h3 id="giving-each-week-a-flavor">Giving Each Week a Flavor</h3>
<p>To avoid the “random LeetCode roulette,” I pick a topic every Sunday night—arrays, trees, dynamic programming, graphs, whatever needs love. Monday is for refreshing the basics, Tuesday through Thursday are for new twists, Friday is my “debug day,” and Saturday mimics a mock interview. Sunday mornings are for a short retrospective and spaced-repetition reviews. Having a theme keeps me from scattering my attention and gives my brain time to actually live with a pattern.</p>
<h2 id="what-does-a-productive-dsa-practice-day-look-like">What does a productive DSA practice day look like?</h2>
<p>Daily DSA isn’t just solving a brand-new problem on a stopwatch. A typical morning might look like this:</p>
<ul>
<li>Warm up by reviewing yesterday’s notes.</li>
<li>Attempt one new problem with a 45-minute timer.</li>
<li>Talk through the solution out loud as if someone were listening.</li>
<li>Log the attempt, including the bug that slowed me down.</li>
<li>Spend five minutes comparing my approach with the editorial to see what I missed.</li>
</ul>
<p>Some days that’s all I have energy for. Other days I’ll throw in a quick pair session with a friend or re-solve an older problem to beat my previous time. The important part is showing up, not putting on a performance.</p>
<h2 id="how-do-you-prevent-dsa-practice-from-becoming-boring">How do you prevent DSA practice from becoming boring?</h2>
<p>I rotate a few extra reps through the week so it doesn’t turn into Groundhog Day:</p>
<ul>
<li>Explaining the day’s solution to an imaginary interviewer while pacing around the living room.</li>
<li>Turning tricky patterns into flashcards so future-me remembers them.</li>
<li>Reviewing yesterday’s code with fresh eyes and cleaning it up as if it were a production PR.</li>
<li>Translating editorial solutions into my own code style instead of copy-pasting.</li>
</ul>
<p>Those small add-ons keep me learning even when the daily problem is an easy one.</p>
<h2 id="how-i-use-coding-assistants-without-cheating-myself">How I Use Coding Assistants Without Cheating Myself</h2>
<p>I do lean on tools, but only after I’ve given the problem a fair shot. My rule is 20 minutes of solo thinking before I ask for a hint. When I do ask, I request a nudge, not the full answer. After I finish, I’ll paste my code into a chat and ask for edge cases I might have overlooked. Occasionally the assistant suggests a cleaner approach, and I write both versions down to compare. The point is to learn, not to win a speedrun.</p>
<h2 id="what-metrics-should-you-track-for-dsa-progress">What metrics should you track for DSA progress?</h2>
<p>Four numbers keep me motivated:</p>
<ul>
<li>The streak count (yes, I still love watching it climb).</li>
<li>Topic coverage, so I don’t hide in array-land forever.</li>
<li>Average confidence score, which tells me when a pattern needs review.</li>
<li>An informal “interview readiness” rating from mock sessions once a month.</li>
</ul>
<p>When my first streak ended at day 94, I was annoyed—but because the routine was simple, I picked it up again the very next day.</p>
<h2 id="what-mistakes-should-you-avoid-when-building-a-dsa-habit">What mistakes should you avoid when building a DSA habit?</h2>
<p>I’ve hit every pothole on this road:</p>
<ul>
<li><strong>Overcooking a single hard problem.</strong> Two hours later, I’d be exhausted and tempted to skip the next day. Now I set a 45-minute limit and come back after learning the official approach.</li>
<li><strong>Grinding random questions without retention.</strong> Weekly themes solved that; it feels more like focused practice than rolling dice.</li>
<li><strong>Solving silently.</strong> The first time I tried to explain a solution out loud, I froze. Now I schedule a mock interview (even if it’s just with a friend on Zoom) every Saturday.</li>
<li><strong>Letting travel or crunch weeks blow up the habit.</strong> Micro fallbacks—like summarizing an editorial on my phone—keep the muscle memory alive when my laptop stays shut.</li>
</ul>
<h2 id="how-does-daily-dsa-practice-improve-real-work-performance">How does daily DSA practice improve real work performance?</h2>
<p>This isn’t just about interviews anymore. Frequent problem solving made me faster at spotting bottlenecks during design reviews. I’m quicker to question a shaky time complexity in code review. When a teammate asks, “Is there a better data structure for this?” I often have an answer ready because I literally practiced that scenario yesterday. And when a bug pops up in a gnarly state machine, I approach it with the same calm curiosity I bring to a puzzle.</p>
<h2 id="what-tools-and-resources-work-best-for-daily-dsa-practice">What tools and resources work best for daily DSA practice?</h2>
<p>I keep my resources unglamorous and reliable:</p>
<ul>
<li><a href="https://leetcode.com/">LeetCode</a> for playlists that align with interview patterns.</li>
<li><a href="https://codeforces.com/">Codeforces</a> when I want a timer and some adrenaline.</li>
<li><a href="https://neetcode.io/">NeetCode</a> for clear pattern explanations.</li>
<li>“Grokking Algorithms” when I need a friendly refresher.</li>
<li>An Anki deck with the handful of patterns I refuse to forget.</li>
<li>A small Discord study group where we swap war stories once a week.</li>
</ul>
<h2 id="if-youre-starting-today">If You’re Starting Today</h2>
<p>Here’s what I wish I’d heard sooner:</p>
<ol>
<li>Pick the smallest daily commitment you know you can keep, even on your worst day.</li>
<li>Write down what happened, especially when it goes sideways.</li>
<li>Stick with one topic for a whole week before jumping to the next.</li>
<li>Get another human involved—pair sessions and mock interviews reveal blind spots instantly.</li>
<li>Celebrate tiny wins. “Accepted in 18 minutes” deserves as much hype as a promotion.</li>
</ol>
<p>If daily DSA still sounds intimidating, pick an easy problem tonight, solve it, and jot down a single sentence about what you learned. Do it again tomorrow. A month from now you’ll have a streak that tells a story about who you’re becoming, not just how many problems you solved.</p>
<p>I’ll be at my desk tomorrow morning with a cup of coffee and a fresh problem. Feel free to join me.</p>
<p>For curated problem lists and structured roadmaps, check out the <a href="https://www.teamblind.com/post/New-Year-Gift---Curated-List-of-Top-75-LeetCode-Questions-to-Save-Your-Time-OaM1orEU">Blind 75</a> and the <a href="https://neetcode.io/roadmap">NeetCode Roadmap</a>. For the science behind habit formation, James Clear’s <a href="https://jamesclear.com/atomic-habits">Atomic Habits framework</a> is an excellent companion read.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-interview-2025">Top Java Interview Questions for 2025</a> — Pair your DSA habit with strong Java fundamentals to ace the full interview loop.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/my-thoughts-on-vibe-coding-2025">My Thoughts on Vibe Coding</a> — The flip side of disciplined practice: how unstructured creative coding keeps the joy alive.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Top Java Interview Questions for 2025: Quick Prep Guide]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/java-interview-2025</link>
      <guid>https://www.rabinarayanpatra.com/blogs/java-interview-2025</guid>
      <pubDate>Wed, 02 Jul 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Currently interviewing for Java roles? Get the essential questions and answers I'm seeing in 2025 interviews. Covers Virtual Threads, modern concurrency, memory management, and Spring Boot internals with practical examples.]]></description>
      <content:encoded><![CDATA[<p>I'm currently in the middle of Java interviews for 2025-level roles, and I've compiled the essential questions I'm seeing—along with concise answers—so you can level up too.</p>
<p>Whether you're facing senior-level concurrency challenges or junior baseline queries, this guide cuts to the chase. Here's what matters right now.</p>
<p>After sitting through about a dozen technical rounds (both as a candidate and helping friends prep), I noticed the same patterns emerging. Interviewers care less about memorizing syntax and more about understanding concepts deeply enough to explain trade-offs and make real-world decisions.</p>
<p>So here's my battle-tested list of questions that actually came up, with the answers that got me through to the next round.</p>
<h2 id="what-core-java-concepts-do-interviewers-still-ask-in-2025">What core Java concepts do interviewers still ask in 2025?</h2>
<h3 id="what-is-java-and-why-is-it-still-relevant">What is Java, and why is it still relevant?</h3>
<p><strong>TL;DR:</strong> Platform-independent, compiled to bytecode, runs on JVM.</p>
<p>Java's "write once, run anywhere" principle comes from its compilation process. When you compile Java code, it becomes bytecode that runs on the Java Virtual Machine, not directly on the operating system. This means the same <code>.class</code> file works on Windows, Linux, and macOS without recompilation.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// This code compiles to bytecode that runs anywhere with a JVM</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> HelloWorld</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Same bytecode, any platform</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Why it still matters in 2025:</strong> Enterprise systems need stability and cross-platform compatibility. Java delivers both, plus it keeps evolving (Virtual Threads, Pattern Matching, Records) while maintaining backward compatibility.</p>
<p><strong>Real-world connection:</strong> In my current role, we deploy the same JAR file to Linux production servers and Windows dev machines. No platform-specific builds needed.</p>
<h3 id="jvm-vs-jre-vs-jdk---whats-the-difference">JVM vs JRE vs JDK - What's the Difference?</h3>
<p><strong>TL;DR:</strong> JDK contains tools to develop, JRE contains runtime to execute, JVM actually runs the bytecode.</p>
<p>This confusion trips up even experienced developers:</p>
<ul>
<li><strong>JDK (Java Development Kit):</strong> Contains everything - compiler (<code>javac</code>), debugger, JRE, documentation, etc.</li>
<li><strong>JRE (Java Runtime Environment):</strong> Just what you need to run Java apps - JVM plus libraries</li>
<li><strong>JVM (Java Virtual Machine):</strong> The actual engine that executes bytecode</li>
</ul>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Development machine needs JDK</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">javac</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> MyApp.java</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    # Compiler is in JDK</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> MyApp</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">          # Runtime is in JRE (which is in JDK)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Production server only needs JRE</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -jar</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> myapp.jar</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> # Just needs the runtime</span></span></code></pre></figure>
<p><strong>Interview tip:</strong> I got asked this as a follow-up to "How would you containerize a Java app?" The answer involves using a minimal JRE-based image for production.</p>
<h2 id="what-java-memory-and-object-creation-questions-appear-in-interviews">What Java memory and object creation questions appear in interviews?</h2>
<h3 id="what-happens-when-you-type-new">What happens when you type <code>new</code>?</h3>
<p><strong>TL;DR:</strong> Allocates memory in heap, calls constructor, returns reference.</p>
<p>Here's the step-by-step process:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">John</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 25</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span></code></pre></figure>
<ol>
<li><strong>Memory allocation:</strong> JVM allocates space in heap for the User object</li>
<li><strong>Zero initialization:</strong> All instance variables set to default values (null, 0, false)</li>
<li><strong>Constructor execution:</strong> Runs the constructor code with provided arguments</li>
<li><strong>Reference assignment:</strong> Variable <code>user</code> gets the memory address</li>
</ol>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Step 2: initialized to null</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Step 2: initialized to 0</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Step 3: constructor runs</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">name </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">age </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Garbage Collection tie-in:</strong> The object stays in heap until no references point to it, then becomes eligible for GC.</p>
<p><strong>What interviewers want to hear:</strong> Understanding that objects live in heap, constructors initialize state, and GC manages cleanup automatically.</p>
<h3 id="heap-vs-stack---where-does-what-go">Heap vs Stack - Where Does What Go?</h3>
<p><strong>TL;DR:</strong> Stack holds method calls and local variables, Heap holds objects and instance variables.</p>
<p>This visual helped me explain it clearly:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processOrder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Order</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // 'order' reference goes on Stack</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> status </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">PENDING</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">           // 'status' reference goes on Stack</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    PaymentInfo</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> PaymentInfo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // PaymentInfo object goes on Heap</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                                              // 'payment' reference goes on Stack</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // order object lives on Heap</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Stack characteristics:</strong></p>
<ul>
<li>Fast allocation/deallocation</li>
<li>Thread-specific (each thread has its own stack)</li>
<li>Limited size (can cause StackOverflowError)</li>
<li>Stores method parameters, local variables, return addresses</li>
</ul>
<p><strong>Heap characteristics:</strong></p>
<ul>
<li>Shared across all threads</li>
<li>Garbage collected</li>
<li>Larger but slower allocation</li>
<li>Stores all objects and instance variables</li>
</ul>
<p><strong>Real interview follow-up:</strong> "What happens if you have a recursive method without a base case?" Answer: StackOverflowError because each recursive call adds a stack frame.</p>
<h2 id="how-do-interviewers-test-concurrency-and-modern-java-knowledge">How do interviewers test concurrency and modern Java knowledge?</h2>
<h3 id="virtual-threads-vs-platform-threads-the-2025-question">Virtual Threads vs Platform Threads (The 2025 Question)</h3>
<p><strong>TL;DR:</strong> Virtual Threads are lightweight, managed by JVM, perfect for I/O-bound tasks. Platform Threads map 1:1 with OS threads.</p>
<p>This is the question I've been asked in every senior-level interview:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Platform Thread approach (old way)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newFixedThreadPool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">200</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // This blocks a precious OS thread during I/O</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> httpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://api.example.com/data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    processResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Virtual Thread approach (new way)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> virtualExecutor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">virtualExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // This parks the virtual thread, freeing the carrier thread</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> httpClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://api.example.com/data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">    processResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p><strong>Key differences:</strong></p>
<table>
<thead>
<tr>
<th>Platform Threads</th>
<th>Virtual Threads</th>
</tr>
</thead>
<tbody>
<tr>
<td>Limited (thousands)</td>
<td>Abundant (millions)</td>
</tr>
<tr>
<td>1:1 with OS threads</td>
<td>Many:few with carrier threads</td>
</tr>
<tr>
<td>Heavy memory overhead</td>
<td>Lightweight</td>
</tr>
<tr>
<td>Good for CPU-bound</td>
<td>Perfect for I/O-bound</td>
</tr>
</tbody>
</table>
<p><strong>When to use what:</strong></p>
<ul>
<li>CPU-intensive: Platform threads</li>
<li>I/O-heavy (database calls, HTTP requests): Virtual threads</li>
<li>Mixed workload: Virtual threads with CPU work offloaded to platform thread pool</li>
</ul>
<h3 id="synchronized-vs-reentrantlock---when-and-why"><code>synchronized</code> vs <code>ReentrantLock</code> - When and Why?</h3>
<p><strong>TL;DR:</strong> <code>synchronized</code> is simpler, <code>ReentrantLock</code> offers more features like timeouts and fair locking.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// synchronized approach</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> synchronizedMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Critical section</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        sharedResource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">update</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // automatically releases lock</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ReentrantLock approach</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ReentrantLock</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ReentrantLock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> lockMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">tryLock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TimeUnit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SECONDS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // Critical section</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            sharedResource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">update</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> finally</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">unlock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // must manually release</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> else</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> TimeoutException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Could not acquire lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Use <code>synchronized</code> when:</strong></p>
<ul>
<li>Simple mutual exclusion</li>
<li>Don't need timeouts or interruption</li>
<li>Want automatic lock release</li>
</ul>
<p><strong>Use <code>ReentrantLock</code> when:</strong></p>
<ul>
<li>Need tryLock with timeout</li>
<li>Want fair locking (longest-waiting thread gets lock first)</li>
<li>Need to interrupt threads waiting for locks</li>
<li>Complex locking patterns</li>
</ul>
<p><strong>Virtual Thread gotcha:</strong> <code>synchronized</code> pins virtual threads to carrier threads, defeating the purpose. Use <code>ReentrantLock</code> with virtual threads.</p>
<h3 id="volatile-threadlocal-and-memory-model-basics">Volatile, ThreadLocal, and Memory Model Basics</h3>
<p><strong>TL;DR:</strong> <code>volatile</code> ensures visibility across threads, <code>ThreadLocal</code> gives each thread its own copy, JMM defines ordering guarantees.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ConcurrencyExample</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> volatile</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> running </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // All threads see updates immediately</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ThreadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> currentUser </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ThreadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Per-thread storage</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> stopExecution</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        running </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Without volatile, other threads might not see this change</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> setCurrentUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">User</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        currentUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Each thread has its own User instance</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getCurrentUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> currentUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Returns this thread's User</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Volatile use cases:</strong></p>
<ul>
<li>Flags that control loop execution</li>
<li>Status variables read by multiple threads</li>
<li>Double-checked locking pattern</li>
</ul>
<p><strong>ThreadLocal use cases:</strong></p>
<ul>
<li>User session information in web applications</li>
<li>Database connections per thread</li>
<li>SimpleDateFormat (not thread-safe, so wrap in ThreadLocal)</li>
</ul>
<p><strong>Memory model essentials:</strong></p>
<ul>
<li>Operations within a thread appear sequential (as-if-serial semantics)</li>
<li>Cross-thread visibility isn't guaranteed without synchronization</li>
<li><code>happens-before</code> relationship ensures ordering</li>
</ul>
<h2 id="what-advanced-java-questions-separate-senior-candidates">What advanced Java questions separate senior candidates?</h2>
<h3 id="records-and-optional---modern-java-done-right">Records and Optional - Modern Java Done Right</h3>
<p><strong>TL;DR:</strong> Records for immutable data, Optional to avoid null pointer exceptions.</p>
<p>Records eliminate boilerplate for data classes:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Old way (before Records)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserOld</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserOld</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">name </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">age </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getAge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Object</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> obj</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> /* boilerplate */</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> hashCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> /* boilerplate */</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> toString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> /* boilerplate */</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Modern way (Records)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Compact constructor for validation</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">age </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IllegalArgumentException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Age cannot be negative</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Custom methods still allowed</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> isAdult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 18</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Optional prevents null pointer exceptions:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Dangerous (can throw NPE)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUserEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toLowerCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // NPE if user is null</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Safe with Optional</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUserEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">String</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">toLowerCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Usage</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUserEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">123L</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ifPresentOrElse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        email </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> sendEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User not found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    );</span></span></code></pre></figure>
<p><strong>Interview tip:</strong> Show you understand when NOT to use Optional (method parameters, fields in classes, collections).</p>
<h3 id="spring-boot-internals---how-annotations-really-work">Spring Boot Internals - How Annotations Really Work</h3>
<p><strong>TL;DR:</strong> Spring uses reflection and proxy pattern to implement dependency injection and AOP.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // How does this work?</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // How does Spring know this is a GET endpoint?</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Transactional</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">       // How does this create a transaction?</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Behind the scenes:</strong></p>
<ol>
<li><strong>Component Scanning:</strong> Spring scans classpath for <code>@Component</code>, <code>@Service</code>, <code>@Controller</code> annotations</li>
<li><strong>Bean Creation:</strong> Creates instances and stores them in ApplicationContext</li>
<li><strong>Dependency Injection:</strong> Uses reflection to inject dependencies into <code>@Autowired</code> fields/constructors</li>
<li><strong>Proxy Creation:</strong> For <code>@Transactional</code>, Spring creates a proxy that wraps your method with transaction logic</li>
</ol>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Simplified version of what Spring does</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> SpringMagicSimplified</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createTransactionalProxy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Object</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Proxy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newProxyInstance</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getClassLoader</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getInterfaces</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">proxy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                // Start transaction</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">                Transaction</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tx </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> transactionManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">begin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">                    Object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">invoke</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">target</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    tx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">commit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    tx</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">rollback</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    throw</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Common follow-up:</strong> "Why doesn't <code>@Transactional</code> work when called from the same class?" Answer: Because Spring's proxy only intercepts external calls, not internal method calls.</p>
<h2 id="what-coding-problems-appear-most-in-java-interviews">What coding problems appear most in Java interviews?</h2>
<h3 id="common-puzzles-that-actually-come-up">Common Puzzles That Actually Come Up</h3>
<p><strong>Reverse a String (with a twist):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Basic version</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> reverse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> StringBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">reverse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Interview twist: "Do it without StringBuilder"</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> reverseManually</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    char</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chars </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toCharArray</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> left </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> right </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chars</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">length </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    while</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">left </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> right</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        char</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> temp </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chars</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">left</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">];</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        chars</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">left</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chars</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">right</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">];</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        chars</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">right</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> temp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        left</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">++</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        right</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">--</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">chars</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Senior-level twist: "Handle Unicode properly"</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> reverseUnicode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> StringBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">str</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">reverse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // StringBuilder handles surrogates correctly</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Detect and Prevent Deadlocks:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Deadlock scenario</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> DeadlockExample</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock1 </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock2 </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> method1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Potential deadlock if another thread locks in reverse order</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                // Critical section</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> method2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Deadlock with method1!</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                // Critical section</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Solution: Always acquire locks in the same order</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> DeadlockSafe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock1 </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock2 </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> method1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Always lock1 first</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                // Critical section</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> method2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Always lock1 first</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                // Critical section</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Implement a Basic LRU Cache:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> LRUCache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> capacity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> head</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> LRUCache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> capacity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">capacity </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> capacity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cache </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> HashMap</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">head </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">tail </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        head</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">next </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        tail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">prev </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> head</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> V</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">K</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> node </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">node </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        moveToHead</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Mark as recently used</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> put</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">K</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> V</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> existing </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">existing </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            existing</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">value </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">            moveToHead</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">existing</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> else</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> newNode </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">put</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> newNode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">            addToHead</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">newNode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> capacity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">                Node</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">K</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> V</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> tail </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> removeTail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">remove</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">tail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Helper methods for doubly linked list operations...</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-performance-and-security-questions-should-you-prepare-for">What performance and security questions should you prepare for?</h2>
<h3 id="memory-leaks--garbage-collection">Memory Leaks &#x26; Garbage Collection</h3>
<p><strong>TL;DR:</strong> Know common leak sources and how to detect them.</p>
<p><strong>Common memory leaks in Java:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// 1. Static collections that grow indefinitely</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> LeakyCache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cache </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> HashMap</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Never cleared!</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> addToCache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">put</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Grows forever</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// 2. Listeners not removed</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> LeakyListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> registerListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        SomeGlobalService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // This creates a reference to the outer class</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">            processEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        });</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Forgot to remove listener - object can't be GC'd</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// 3. ThreadLocal not cleaned up</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> LeakyThreadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ThreadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ExpensiveObject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> threadLocal </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ThreadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> doWork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        threadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ExpensiveObject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Forgot to call threadLocal.remove() - memory leak in thread pools</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Detection strategies:</strong></p>
<ul>
<li>Heap dumps (<code>jmap</code>, VisualVM, Eclipse MAT)</li>
<li>GC logs (<code>-XX:+PrintGCDetails</code>)</li>
<li>Application monitoring (Micrometer, AppDynamics)</li>
</ul>
<p><strong>GC tuning basics:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># G1GC (good default for most applications)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -XX:+UseG1GC</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -XX:MaxGCPauseMillis=200</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> MyApp</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># ZGC (ultra-low latency)</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">java</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -XX:+UnlockExperimentalVMOptions</span><span style="--shiki-dark:#C3E88D;--shiki-light:#005CC5"> -XX:+UseZGC</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> MyApp</span></span></code></pre></figure>
<h3 id="deserialization-vulnerabilities">Deserialization Vulnerabilities</h3>
<p><strong>TL;DR:</strong> Never deserialize untrusted data without validation.</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Dangerous - can lead to RCE</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> deserializeUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">byte</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ObjectInputStream</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ois </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ObjectInputStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ByteArrayInputStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ois</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">readObject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Attacker can execute arbitrary code!</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Safer approach - validate before deserializing</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> deserializeUserSafely</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    ObjectMapper</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> mapper </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ObjectMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    mapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">configure</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">DeserializationFeature</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">FAIL_ON_UNKNOWN_PROPERTIES</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> mapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">readValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">json</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        validateUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">  // Business logic validation</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">JsonProcessingException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> InvalidDataException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Invalid user data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Best approach - use Records with validation</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ValidatedUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NotBlank</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Email</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Min</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">13</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> age</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ValidatedUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Compact constructor validation</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requireNonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Username required</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requireNonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Email required</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-should-you-structure-your-java-interview-answers">How should you structure your Java interview answers?</h2>
<h3 id="how-to-structure-your-answers">How to Structure Your Answers</h3>
<p><strong>The winning formula:</strong></p>
<ol>
<li><strong>TL;DR</strong> - Give the short answer first</li>
<li><strong>Explain the concept</strong> - Show you understand the underlying principles</li>
<li><strong>Provide an example</strong> - Code snippet or real-world scenario</li>
<li><strong>Mention trade-offs</strong> - Show you think about pros/cons</li>
<li><strong>Connect to experience</strong> - "In my last project, we used this because..."</li>
</ol>
<h3 id="before-you-answer-clarify-assumptions">Before You Answer, Clarify Assumptions</h3>
<p><strong>Good questions to ask:</strong></p>
<ul>
<li>"Are we talking about single-threaded or multi-threaded environment?"</li>
<li>"Should I optimize for memory or speed?"</li>
<li>"What's the expected scale - thousands or millions of operations?"</li>
<li>"Are there any constraints on external libraries?"</li>
</ul>
<h3 id="connect-everything-to-real-world-experience">Connect Everything to Real-World Experience</h3>
<p>Instead of: "HashMap uses array of buckets with linked lists."</p>
<p>Try: "HashMap uses array of buckets with linked lists. In our user session cache, we chose HashMap over TreeMap because we needed O(1) lookups and didn't care about ordering. But we had to be careful about thread safety since multiple requests could access the same session data."</p>
<h3 id="red-flags-to-avoid">Red Flags to Avoid</h3>
<p>❌ <strong>"I don't know"</strong> (without trying to reason through it)
✅ <strong>"I'm not certain, but here's how I'd approach it..."</strong></p>
<p>❌ <strong>Memorized answers without understanding</strong>
✅ <strong>Explaining the reasoning behind design decisions</strong></p>
<p>❌ <strong>Only theoretical knowledge</strong>
✅ <strong>Connecting concepts to practical experience</strong></p>
<hr>
<h2 id="the-questions-you-should-ask-them">The Questions You Should Ask Them</h2>
<p>Turn the interview around with smart questions:</p>
<ul>
<li>"What's your approach to handling technical debt?"</li>
<li>"How do you balance feature delivery with code quality?"</li>
<li>"What's the most interesting technical challenge the team solved recently?"</li>
<li>"How do you stay current with Java's rapid release cycle?"</li>
</ul>
<p>Remember, you're interviewing them too. A company that can't answer these thoughtfully might not be the right fit.</p>
<hr>
<p><strong>Quick confession:</strong> I bombed my first few 2025 interviews because I focused too much on memorizing APIs instead of understanding concepts. Once I shifted to explaining trade-offs and connecting everything to real experience, the conversations became much more natural.</p>
<p>The Java ecosystem keeps evolving, but the fundamentals—understanding memory, concurrency, and writing maintainable code—never go out of style. Master these concepts, practice explaining them clearly, and you'll do great.</p>
<h2 id="further-reading">Further Reading</h2>
<ul>
<li><a href="https://docs.oracle.com/en/java/javase/21/">Java SE 21 Documentation</a> — Official Java 21 language and API reference</li>
<li><a href="https://www.oreilly.com/library/view/effective-java/9780134686097/">Effective Java, 3rd Edition</a> by Joshua Bloch — Essential reading for Java best practices</li>
<li><a href="https://jcip.net/">Java Concurrency in Practice</a> by Brian Goetz — The definitive guide to Java threading and concurrency</li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Mastering Virtual Threads in Java 25</a> — Deep dive into the concurrency feature interviewers are asking about most in 2025.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/why-1-1-is-true-but-128-128-is-false-in-java">Why <code>1 == 1</code> is True but <code>128 == 128</code> is False in Java</a> — A classic gotcha that keeps showing up in interviews; understand the Integer caching trap.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/how-i-turned-daily-problem-solving-into-a-dsa-habit">How I Turned Daily Problem Solving into a DSA Habit</a> — Complement your Java knowledge with a consistent DSA practice routine.</li>
</ul>
<p><strong>Good luck with your interviews!</strong> 🚀</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Java 25 Virtual Threads: A Practical Guide]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25</link>
      <guid>https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25</guid>
      <pubDate>Wed, 02 Jul 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[A practical guide to Java 25 virtual threads: how they work, structured concurrency, the pinning pitfalls to avoid, and when they beat platform threads.]]></description>
      <content:encoded><![CDATA[<p>Remember when handling 10,000 concurrent users meant provisioning servers with massive thread pools and carefully tuning connection limits? Those days are over. Java 25's Virtual Threads have fundamentally changed how we think about concurrency, and honestly, it's the biggest leap forward in Java threading since... well, since threads were invented.</p>
<p>I've been working with Virtual Threads since their preview days, and I can tell you this: once you experience writing concurrent code that just <em>works</em> without the complexity, you'll never want to go back to traditional threading models.</p>
<p>In this deep-dive guide, we'll explore everything you need to know about Virtual Threads in Java 25, from the basics to advanced patterns that will make your applications lightning-fast and incredibly scalable.</p>
<h2 id="what-are-virtual-threads-and-why-should-you-care">What Are Virtual Threads? (And Why Should You Care?)</h2>
<p>Think of Virtual Threads as lightweight threads that are managed by the JVM rather than the operating system. While platform threads have a 1:1 mapping with OS threads (expensive and limited), Virtual Threads are cheap, abundant, and designed for blocking operations.</p>
<p>Here's the mind-blowing part: you can create <strong>millions</strong> of Virtual Threads without breaking a sweat. Your laptop can handle what previously required entire server farms.</p>
<h3 id="the-problem-virtual-threads-solve">The Problem Virtual Threads Solve</h3>
<p>Let's start with a real-world scenario. Imagine you're building an e-commerce API that needs to:</p>
<ul>
<li>Fetch user data from a database</li>
<li>Call a payment service</li>
<li>Update inventory</li>
<li>Send confirmation emails</li>
<li>Log analytics events</li>
</ul>
<p><strong>Traditional Threading Approach:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> OrderController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> OrderService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // This approach ties up precious platform threads</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PostMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/orders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">OrderResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createOrder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestBody</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> OrderRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Thread blocks here waiting for database</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUserId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Thread blocks again for payment processing</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        PaymentResult</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> paymentService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">processPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Another blocking call</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        inventoryService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">reserveItems</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getItems</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // And another...</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Order</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createOrder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getItems</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">OrderResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The problem? Each request consumes a platform thread for its entire lifecycle. With traditional thread pools, you might handle 200-500 concurrent requests before running into thread exhaustion.</p>
<p><strong>Virtual Threads Approach:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> OrderController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> OrderService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Same code, but now it runs on Virtual Threads!</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PostMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/orders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">OrderResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createOrder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestBody</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> OrderRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Virtual Thread parks during database call - carrier thread is freed</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUserId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Parks again during payment - no platform thread blocked</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        PaymentResult</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> paymentService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">processPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Virtual Thread continues seamlessly</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        inventoryService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">reserveItems</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getItems</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Order</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> order </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orderService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createOrder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getItems</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">OrderResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">from</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Same code, dramatically different behavior. Now you can handle 100,000+ concurrent requests on the same hardware.</p>
<h2 id="how-do-you-create-and-run-your-first-virtual-thread-in-java">How do you create and run your first Virtual Thread in Java?</h2>
<p>Enabling Virtual Threads in Java 25 is surprisingly simple:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> VirtualThreadBasics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> InterruptedException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Create a Virtual Thread the simple way</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Thread</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> vt1 </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofVirtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Running on: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentThread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Or use a factory for multiple threads</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        ThreadFactory</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofVirtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Thread</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> vt2 </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newThread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">out</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">println</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Another virtual thread: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentThread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        vt2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Wait for completion</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        vt1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        vt2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>For Spring Boot applications, enable Virtual Threads globally:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">SpringBootApplication</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Application</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Enable Virtual Threads for all web requests</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setProperty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">spring.threads.virtual.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        SpringApplication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Application</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>That's it. Every web request now runs on a Virtual Thread automatically.</p>
<h2 id="how-do-you-build-a-high-throughput-api-with-virtual-threads">How do you build a high-throughput API with Virtual Threads?</h2>
<p>Let's build something practical - a social media feed aggregator that fetches data from multiple sources:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Slf4j</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> FeedAggregatorService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">SocialMediaClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> clients</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> virtualExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> FeedAggregatorService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">SocialMediaClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> clients</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">clients </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> clients</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Create an executor that uses Virtual Threads</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">virtualExecutor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AggregatedFeed</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getFeed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Fetching feed for user: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Launch concurrent requests to all social media platforms</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">FeedData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> futures </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> clients</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">debug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Fetching from {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPlatformName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUserFeed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Failed to fetch from {}: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                             client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPlatformName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> FeedData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">empty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">client</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPlatformName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            },</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> virtualExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Wait for all responses (or timeout)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">FeedData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> feeds </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> futures</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">future </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TimeUnit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SECONDS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Timeout or error getting feed data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> FeedData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">empty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">unknown</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            })</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> mergeFeedsIntelligently</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">feeds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> AggregatedFeed</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> mergeFeedsIntelligently</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">FeedData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> feeds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Merge posts by timestamp, apply user preferences, remove duplicates</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> allPosts </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> feeds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">flatMap</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">feed </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> feed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPosts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sorted</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Comparator</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">comparing</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Post</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getTimestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">reversed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">distinct</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> AggregatedFeed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">allPosts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Example client implementation</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TwitterClient</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> SocialMediaClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> RestTemplate</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> restTemplate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> FeedData</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUserFeed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // This HTTP call will park the Virtual Thread, not block a platform thread</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> url </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://api.twitter.com/2/users/{userId}/tweets?max_results={limit}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            TwitterResponse</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> restTemplate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getForObject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TwitterResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> posts </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> response</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">tweet </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Post</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    tweet</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    tweet</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getText</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                    "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">twitter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    tweet</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCreatedAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> FeedData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">twitter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> posts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Error fetching Twitter feed for user {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> SocialMediaException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Twitter API error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getPlatformName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">twitter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The beautiful thing about this code? It looks like synchronous, blocking code, but it's actually highly concurrent. When each <code>restTemplate.getForObject()</code> call happens, the Virtual Thread parks itself, freeing up the carrier thread to handle other work.</p>
<h2 id="how-much-faster-are-virtual-threads-compared-to-platform-threads">How much faster are Virtual Threads compared to platform threads?</h2>
<p>I ran some benchmarks comparing traditional threads vs Virtual Threads for a typical web service scenario:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Slf4j</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PerformanceBenchmark</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Simulate a typical web service call pattern</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> simulateWebServiceCall</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // Database query (100ms average)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sleep</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // External API call (200ms average)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sleep</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">200</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // Cache operation (50ms average)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sleep</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">50</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // Business logic (10ms average)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sleep</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">10</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">InterruptedException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentThread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">interrupt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> benchmarkPlatformThreads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newFixedThreadPool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">200</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> start </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentTimeMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> futures </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> IntStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">mapToObj</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">this</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">simulateWebServiceCall</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        futures</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">future </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Error in platform thread execution</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> duration </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentTimeMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Platform threads: {} requests in {}ms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">shutdown</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> benchmarkVirtualThreads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> start </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentTimeMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> futures </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> IntStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">range</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">mapToObj</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">i </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">this</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">simulateWebServiceCall</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        futures</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">future </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                future</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Error in virtual thread execution</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> duration </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentTimeMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Virtual threads: {} requests in {}ms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        executor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">shutdown</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Results (10,000 requests):</strong></p>
<ul>
<li>Platform threads (200 pool): ~18 seconds</li>
<li>Virtual threads: ~360 milliseconds</li>
</ul>
<p>That's a <strong>50x improvement</strong> in throughput, and memory usage was dramatically lower with Virtual Threads.</p>
<h2 id="what-advanced-patterns-work-best-with-virtual-threads">What advanced patterns work best with Virtual Threads?</h2>
<h3 id="pattern-1-fan-outfan-in-operations">Pattern 1: Fan-Out/Fan-In Operations</h3>
<p>Perfect for scenarios where you need to call multiple services and aggregate results:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ProductEnrichmentService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> EnrichedProduct</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> enrichProduct</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Start all enrichment operations concurrently</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pricesFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            priceService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCurrentPricing</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reviewsFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            reviewService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getProductReviews</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> inventoryFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            inventoryService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAvailability</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        var</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> recommendationsFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            recommendationService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getSimilarProducts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Wait for all operations to complete</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Void</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> allFutures </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">allOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            pricesFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reviewsFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> inventoryFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> recommendationsFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            allFutures</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TimeUnit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SECONDS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> EnrichedProduct</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                pricesFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                reviewsFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                inventoryFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                recommendationsFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">TimeoutException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Timeout enriching product {}, returning partial data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> buildPartialProduct</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> pricesFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> reviewsFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                                     inventoryFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> recommendationsFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Error enriching product {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> productId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ProductEnrichmentException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Failed to enrich product</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="pattern-2-streaming-data-processing">Pattern 2: Streaming Data Processing</h3>
<p>Virtual Threads excel at handling streaming scenarios:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> StreamProcessor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processEventStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> streamName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Each event is processed on its own Virtual Thread</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> processor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        eventStreamSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">connect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">streamName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> processor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">submit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                    // Complex processing that might involve I/O</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">                    ProcessedEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> processed </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                    // Save to database (blocking operation)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    eventRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">processed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                    // Send notification (another blocking operation)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    notificationService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sendNotification</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">processed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                    // Update metrics</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                    updateMetrics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">processed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Failed to process event {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    deadLetterQueue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">send</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ProcessedEvent</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Event</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Simulate complex processing</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> switch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> USER_ACTION </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processUserAction</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> SYSTEM_EVENT </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processSystemEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> EXTERNAL_WEBHOOK </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processWebhook</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UnsupportedEventTypeException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unknown event type: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getType</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        };</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-are-the-best-practices-for-using-virtual-threads">What are the best practices for using Virtual Threads?</h2>
<h3 id="1-dont-use-thread-pools-seriously">1. Don't Use Thread Pools (Seriously)</h3>
<p>With Virtual Threads, the old "limited thread pool" thinking goes out the window:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ❌ DON'T: Create limited pools of Virtual Threads</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> badExecutor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newFixedThreadPool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">10</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofVirtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ✅ DO: Use unlimited Virtual Thread executors</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> goodExecutor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ✅ OR: Create Virtual Threads directly</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofVirtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Your code here</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">});</span></span></code></pre></figure>
<h3 id="2-avoid-cpu-intensive-tasks">2. Avoid CPU-Intensive Tasks</h3>
<p>Virtual Threads are designed for I/O-bound work. For CPU-intensive tasks, stick with platform threads:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> TaskDispatcher</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cpuExecutor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newFixedThreadPool</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Runtime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getRuntime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">availableProcessors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ExecutorService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ioExecutor </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ProcessingResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processTask</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Task</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> task</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">task</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isCpuIntensive</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // Use platform threads for CPU work</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                performCpuIntensiveWork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">task</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cpuExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> else</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">            // Use Virtual Threads for I/O work</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                performIoWork</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">task</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ioExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="3-be-careful-with-threadlocal">3. Be Careful with ThreadLocal</h3>
<p>ThreadLocal with Virtual Threads can lead to memory issues since there can be millions of them:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ❌ DON'T: Use ThreadLocal with Virtual Threads for large data</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ThreadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> USER_CONTEXT </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ThreadLocal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ✅ DO: Use ScopedValues (Java 25 preview feature)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ScopedValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> USER_CONTEXT </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    ScopedValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newInstance</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpServletRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    UserContext</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> context </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> extractUserContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    ScopedValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">runWhere</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">USER_CONTEXT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> context</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> ()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Code that needs access to user context</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        processRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    });</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="4-monitor-virtual-thread-usage">4. Monitor Virtual Thread Usage</h3>
<p>Keep track of your Virtual Thread metrics:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> VirtualThreadMonitor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> MeterRegistry</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EventListener</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Async</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> monitorVirtualThreads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ApplicationReadyEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Gauge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">virtual.threads.active</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Number of active Virtual Threads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">register</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> VirtualThreadMonitor</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getActiveVirtualThreads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> double</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getActiveVirtualThreads</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">VirtualThreadMonitor</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> monitor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Thread</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAllStackTraces</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">keySet</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">filter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Thread</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">isVirtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">count</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-are-the-most-common-virtual-thread-mistakes-and-how-do-you-fix-them">What are the most common Virtual Thread mistakes and how do you fix them?</h2>
<h3 id="pitfall-1-blocking-synchronized-operations">Pitfall 1: Blocking Synchronized Operations</h3>
<p>Virtual Threads can't park during synchronized blocks, which defeats the purpose:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ❌ BAD: Synchronized blocks pin Virtual Threads</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Object</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> badMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    synchronized</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // This pins the Virtual Thread to its carrier</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        callBlockingService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Platform thread stays blocked</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ✅ GOOD: Use java.util.concurrent locks</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ReentrantLock</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lock </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ReentrantLock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> goodMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Virtual Thread can park and unpark during blocking operations</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        callBlockingService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> finally</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        lock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">unlock</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="pitfall-2-not-configuring-carrier-thread-pool">Pitfall 2: Not Configuring Carrier Thread Pool</h3>
<p>For applications with specific requirements, tune the carrier thread pool:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Configure carrier thread pool size</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setProperty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">jdk.virtualThreadScheduler.parallelism</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">8</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Or programmatically</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> VirtualThreadConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Primary</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Executor</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> taskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="pitfall-3-overusing-virtual-threads-for-short-tasks">Pitfall 3: Overusing Virtual Threads for Short Tasks</h3>
<p>For very short-lived tasks, the overhead of creating Virtual Threads might not be worth it:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ❌ Overkill for simple operations</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> results </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">item </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        simpleTransformation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">item</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> virtualExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">join</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// ✅ Use parallel streams for CPU-bound transformations</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> results </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">parallelStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">this</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">simpleTransformation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span></code></pre></figure>
<h2 id="how-do-you-configure-virtual-threads-in-a-production-spring-boot-application">How do you configure Virtual Threads in a production Spring Boot application?</h2>
<p>Here's how to properly configure Virtual Threads in a production Spring Boot application:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableAsync</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableScheduling</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> VirtualThreadConfiguration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">virtualTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Primary</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Executor</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> virtualTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> TomcatProtocolHandlerCustomizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> protocolHandlerVirtualThreadExecutorCustomizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> protocolHandler </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            protocolHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Executors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newVirtualThreadPerTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        };</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ConditionalOnProperty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">app.virtual-threads.web.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> havingValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> WebMvcConfigurer</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> webMvcConfigurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> WebMvcConfigurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> configureAsyncSupport</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">AsyncSupportConfigurer</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">virtualTaskExecutor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                configurer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setDefaultTimeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">30000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        };</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Application properties</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"># application</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">yml</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">app</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  virtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">threads</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    web</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      enabled</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">spring</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  threads</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    virtual</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      enabled</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">  task</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">    execution</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">      pool</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        virtual</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">threads</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span></span></code></pre></figure>
<h2 id="why-do-virtual-threads-matter-for-the-future-of-java">Why do Virtual Threads matter for the future of Java?</h2>
<p>Virtual Threads represent a fundamental shift in how we think about concurrency in Java. They solve the C10K problem (handling 10,000 concurrent connections) and push us well into the C1M territory (1 million concurrent connections).</p>
<p>For most applications, this means:</p>
<ul>
<li><strong>Simpler Code</strong>: Write straightforward blocking code that performs like async code</li>
<li><strong>Better Resource Utilization</strong>: Handle more concurrent users with less hardware</li>
<li><strong>Improved Reliability</strong>: Fewer thread pool exhaustion issues</li>
<li><strong>Easier Debugging</strong>: Call stacks that actually make sense</li>
</ul>
<p>The best part? You can often get these benefits by changing just a few configuration lines in existing applications.</p>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>Virtual Threads aren't just another Java feature - they're a paradigm shift that makes concurrent programming accessible to every Java developer. You no longer need to be a concurrency expert to write highly scalable applications.</p>
<p>Start small: enable Virtual Threads in a non-critical service and watch your throughput numbers. Once you see a 10x-50x improvement in concurrent request handling, you'll understand why this is the future of Java.</p>
<p>The era of thread pools, careful resource management, and complex async programming is coming to an end. Welcome to the age of Virtual Threads, where concurrent programming is finally simple again.</p>
<hr>
<p><em>Have you started using Virtual Threads in your projects? I'd love to hear about your experiences and any patterns you've discovered. Drop a comment below and let's discuss!</em></p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://openjdk.org/jeps/444">JEP 444: Virtual Threads</a> — The official JDK Enhancement Proposal for Virtual Threads</li>
<li><a href="https://wiki.openjdk.org/display/loom">Project Loom Wiki</a> — OpenJDK's Loom project documentation</li>
<li><a href="https://docs.spring.io/spring-framework/reference/integration/observability.html">Spring Framework Virtual Thread Support</a> — Official Spring documentation on Virtual Thread integration</li>
<li><a href="https://jcip.net/">Java Concurrency in Practice</a> — Brian Goetz's foundational resource on Java threading</li>
</ul>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-25-compact-object-headers">Java 25 Compact Object Headers: Save 20% Memory</a> — Another major Java 25 performance win that pairs perfectly with Virtual Threads.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-interview-2025">Top Java Interview Questions for 2025</a> — Virtual Threads are a hot interview topic; this guide covers what you need to know.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot">Modern Java Development with Spring Boot</a> — See how Virtual Threads fit into a broader modern Java and Spring Boot stack.</li>
</ul>
<p><strong>Happy threading!</strong> 🧵✨</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Modern Java Development: Mastering Java 21+ Features and Spring Boot Best Practices in 2025]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot</link>
      <guid>https://www.rabinarayanpatra.com/blogs/modern-java-spring-boot</guid>
      <pubDate>Mon, 30 Jun 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Discover the latest Java 21+ features including Virtual Threads, Pattern Matching, and Records. Learn advanced Spring Boot best practices, performance optimizations, and modern architectural patterns that every Java developer should know.]]></description>
      <content:encoded><![CDATA[<p>Java continues to evolve at an unprecedented pace, with new features being released every six months. Yet many developers are still stuck using Java 8 patterns and missing out on powerful modern features that can dramatically improve code quality, performance, and developer productivity.</p>
<p>In this comprehensive guide, we'll explore <strong>Java 21's revolutionary features</strong> and dive deep into <strong>Spring Boot best practices</strong> that most developers overlook. You'll learn how to leverage Virtual Threads, Pattern Matching, Records, and advanced Spring Boot techniques to build high-performance, maintainable applications.</p>
<h2 id="what-youll-master-in-this-guide">What You'll Master in This Guide</h2>
<p>By the end of this tutorial, you'll have learned:</p>
<ul>
<li>🚀 <strong>Virtual Threads</strong> for massive concurrency improvements</li>
<li>🔍 <strong>Pattern Matching</strong> and Switch Expressions for cleaner code</li>
<li>📊 <strong>Records</strong> and their advanced use cases beyond simple DTOs</li>
<li>⚡ <strong>Spring Boot 3.2+</strong> performance optimizations</li>
<li>🏗️ <strong>Modern architectural patterns</strong> with Spring Boot</li>
<li>🛡️ <strong>Security best practices</strong> for production applications</li>
<li>📈 <strong>Observability</strong> and monitoring with Micrometer</li>
<li>🐳 <strong>Containerization</strong> strategies for Java applications</li>
<li>🔧 <strong>Advanced configuration</strong> techniques</li>
<li>📱 <strong>Reactive programming</strong> with Spring WebFlux</li>
</ul>
<h2 id="what-makes-java-21-a-game-changing-lts-release">What makes Java 21 a game-changing LTS release?</h2>
<p>Java 21 is the latest Long Term Support (LTS) release, packed with features that fundamentally change how we write Java code. Let's explore the most impactful ones:</p>
<h3 id="virtual-threads-revolutionizing-concurrency">Virtual Threads: Revolutionizing Concurrency</h3>
<p>Virtual Threads are perhaps the most significant addition to Java in years. They allow you to create millions of lightweight threads without the traditional overhead.</p>
<p><strong>Before Virtual Threads (Traditional Approach):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/users/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // This blocks a platform thread while waiting for I/O</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ExternalApiClient</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> apiClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Each I/O operation blocks a precious platform thread</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">orElseThrow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserNotFoundException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User not found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // External API call blocks thread</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        UserProfile</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profile </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> apiClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUserProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">profile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>With Virtual Threads (Modern Approach):</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">SpringBootApplication</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Application</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> main</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">[]</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Enable Virtual Threads globally</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        System</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setProperty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">spring.threads.virtual.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        SpringApplication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">run</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Application</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> args</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/users/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Now runs on a virtual thread - can handle millions of concurrent requests</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/users/batch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUsers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestParam</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Long</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ids</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Process thousands of requests concurrently</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> users </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ids</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">parallelStream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userService</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">collect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Collectors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toList</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ExternalApiClient</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> apiClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Each virtual thread can handle blocking I/O efficiently</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> enrichUserData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">orElseThrow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserNotFoundException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User not found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> enrichUserData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">User</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Multiple I/O operations that would block platform threads</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Now run efficiently on virtual threads</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profileFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> apiClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUserProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ordersFuture </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            CompletableFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">supplyAsync</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> apiClient</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUserOrders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Combine results without blocking threads unnecessarily</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            UserProfile</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profile </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profileFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TimeUnit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SECONDS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Order</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> orders </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ordersFuture</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TimeUnit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SECONDS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">profile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setRecentOrders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">orders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Failed to enrich user data</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="pattern-matching-and-switch-expressions">Pattern Matching and Switch Expressions</h3>
<p>Pattern matching makes code more readable and less error-prone:</p>
<p><strong>Old Java Approach:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PaymentProcessor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PaymentResult</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Payment</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">payment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">instanceof</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CreditCardPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            CreditCardPayment</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ccPayment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">CreditCardPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processCreditCard</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ccPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ccPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAmount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> else</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">payment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">instanceof</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PayPalPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            PayPalPayment</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ppPayment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">PayPalPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processPayPal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ppPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ppPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAmount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> else</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">payment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">instanceof</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> BankTransferPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            BankTransferPayment</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> btPayment </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BankTransferPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processBankTransfer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">btPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAccountNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> btPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAmount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> else</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UnsupportedPaymentMethodException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unsupported payment method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Modern Java 21 Approach:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PaymentProcessor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PaymentResult</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> processPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Payment</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> switch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CreditCardPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var expiryDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                processCreditCard</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> PayPalPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                processPayPal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> BankTransferPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var accountNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var routingNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                processBankTransfer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">accountNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CryptoPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var walletAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var currency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                when currency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">BTC</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                processBitcoin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">walletAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CryptoPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var walletAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var currency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                processCrypto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">walletAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> currency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UnsupportedPaymentMethodException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unsupported payment method: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getSimpleName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        };</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Advanced pattern matching for validation</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ValidationResult</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> validatePayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Payment</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> switch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CreditCardPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var expiry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                when amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">compareTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BigDecimal</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ZERO</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x3C;=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                ValidationResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">invalid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Amount must be positive</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CreditCardPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var expiry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                when </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isValidCardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                ValidationResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">invalid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Invalid card number</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CreditCardPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var expiry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                when expiry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isBefore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LocalDate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                ValidationResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">invalid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Card expired</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> PayPalPayment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">var email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> var amount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                when </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isValidEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                ValidationResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">invalid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Invalid email format</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            case</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Payment</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> p when p</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAmount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">compareTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">MAX_AMOUNT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ></span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                ValidationResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">invalid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Amount exceeds maximum limit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            default</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ValidationResult</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">valid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        };</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="records-beyond-simple-dtos">Records: Beyond Simple DTOs</h3>
<p>Records are perfect for immutable data classes, but they can do much more:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Basic Record for API responses</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    LocalDateTime</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createdAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    UserProfile</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profile</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Custom constructor with validation</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requireNonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Username cannot be null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requireNonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Email cannot be null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">trim</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEmpty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IllegalArgumentException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Username cannot be empty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Static factory methods</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserResponse</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> fromEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">User</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCreatedAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Computed properties</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> displayName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profile </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fullName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> !=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            ?</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> profile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fullName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            :</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Business logic methods</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> isRecent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createdAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isAfter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">LocalDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">minusDays</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Records for configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> DatabaseConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> maxPoolSize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Duration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> connectionTimeout</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> DatabaseConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">maxPoolSize </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IllegalArgumentException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Max pool size must be positive</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">connectionTimeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isNegative</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> IllegalArgumentException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Connection timeout cannot be negative</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Default configuration</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> DatabaseConfig</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> defaultConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> DatabaseConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">jdbc:postgresql://localhost:5432/myapp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">app_user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">            10</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofSeconds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Records for complex data structures</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PaginatedResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">>(</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> content</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> totalElements</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> hasNext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> hasPrevious</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PaginatedResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">T</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PaginatedResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getContent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getSize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getTotalElements</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasNext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            page</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasPrevious</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> totalPages</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">int</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Math</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ceil</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">((</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">double</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> totalElements </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-spring-boot-best-practices-do-most-developers-miss">What Spring Boot best practices do most developers miss?</h2>
<h3 id="1-proper-configuration-management">1. Proper Configuration Management</h3>
<p>Most developers don't leverage Spring Boot's powerful configuration features:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Instead of scattered @Value annotations</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> EmailService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">${email.smtp.host}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> smtpHost</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">${email.smtp.port}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> smtpPort</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">${email.from}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fromAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ... rest of the messy code</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Use Configuration Properties (BEST PRACTICE)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ConfigurationProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">prefix</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Validated</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> EmailProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NotBlank</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fromAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Valid</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SmtpConfig</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> smtp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Valid</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> RetryConfig</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> retry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> enabled</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> SmtpConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NotBlank</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> host</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Min</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Max</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">65535</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> port</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> startTls</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> password</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> RetryConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Min</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> maxAttempts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">NotNull</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Duration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> initialDelay</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DecimalMin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> double</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> multiplier</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {}</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> EmailService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> EmailProperties</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> emailProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> JavaMailSender</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> mailSender</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Retryable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        retryFor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> MailException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        maxAttemptsExpression</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#{@emailProperties.retry().maxAttempts()}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">        backoff</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Backoff</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">            delayExpression</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#{@emailProperties.retry().initialDelay().toMillis()}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">            multiplierExpression</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#{@emailProperties.retry().multiplier()}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        )</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> sendEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> to</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> subject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">emailProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Email sending is disabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        SimpleMailMessage</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> message </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> SimpleMailMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setFrom</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">emailProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fromAddress</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">to</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setSubject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">subject</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setText</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        mailSender</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">send</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Email sent successfully to {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> to</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="2-advanced-exception-handling">2. Advanced Exception Handling</h3>
<p>Create a robust error handling system:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Global exception handler with proper error responses</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestControllerAdvice</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Slf4j</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> GlobalExceptionHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ExceptionHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ValidationException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ResponseStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BAD_REQUEST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleValidation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ValidationException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Validation error: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">BAD_REQUEST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Validation Failed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCurrentPath</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">validationErrors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getFieldErrors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ExceptionHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">EntityNotFoundException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ResponseStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">NOT_FOUND</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleNotFound</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">EntityNotFoundException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Entity not found: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">NOT_FOUND</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Resource Not Found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCurrentPath</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ExceptionHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">OptimisticLockingFailureException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ResponseStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">CONFLICT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleOptimisticLocking</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">OptimisticLockingFailureException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Optimistic locking failure: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMessage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">CONFLICT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Concurrent Modification</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">The resource was modified by another user. Please refresh and try again.</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCurrentPath</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ExceptionHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ResponseStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">INTERNAL_SERVER_ERROR</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleGeneral</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> errorId </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> UUID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">randomUUID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unexpected error [{}]: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> errorId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ex</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ErrorResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">INTERNAL_SERVER_ERROR</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Internal Server Error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">An unexpected error occurred. Error ID: </span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> errorId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getCurrentPath</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getCurrentPath</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> RequestContextHolder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">currentRequestAttributes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAttribute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">RequestAttributes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">REFERENCE_REQUEST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> RequestAttributes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SCOPE_REQUEST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Custom error response record</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> record</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ErrorResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Instant</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    int</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> message</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> path</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    Map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> validationErrors</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ErrorResponseBuilder</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> ErrorResponseBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ErrorResponseBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Builder pattern implementation</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="3-performance-optimization-with-caching">3. Performance Optimization with Caching</h3>
<p>Implement intelligent caching strategies:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableCaching</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ConditionalOnProperty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">cache.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> havingValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> matchIfMissing</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CacheConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CacheManager</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> cacheManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CaffeineCacheManager</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cacheManager </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CaffeineCacheManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        cacheManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setCaffeine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">caffeineCacheBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cacheManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Caffeine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> Object</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> caffeineCacheBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Caffeine</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">newBuilder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">initialCapacity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">maximumSize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">expireAfterAccess</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofMinutes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">10</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">expireAfterWrite</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofMinutes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">recordStats</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ConditionalOnProperty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">cache.redis.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> havingValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> RedisCacheManager</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> redisCacheManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">RedisConnectionFactory</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        RedisCacheConfiguration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> config </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> RedisCacheConfiguration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">defaultCacheConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">entryTtl</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ofMinutes</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">30</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">serializeKeysWith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">RedisSerializationContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SerializationPair</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fromSerializer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> StringRedisSerializer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">serializeValuesWith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">RedisSerializationContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SerializationPair</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fromSerializer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> GenericJackson2JsonRedisSerializer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> RedisCacheManager</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">factory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">cacheDefaults</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Slf4j</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserMapper</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Cacheable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> unless</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#result == null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">debug</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Fetching user with id: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userMapper</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">toResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Cacheable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user-profiles</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserProfile</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUserProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findByUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">this</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">buildUserProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">orElse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">CacheEvict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user-profiles</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">},</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#user.id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserResponse</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> updateUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">User</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> savedUser </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Updated user: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> savedUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">savedUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Caching</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">evict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">CacheEvict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> key</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">#id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">CacheEvict</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">value</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">user-profiles</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> allEntries</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    })</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> deleteUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">deleteById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Deleted user: {}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Cache warming strategy</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EventListener</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Async</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleApplicationReady</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">ApplicationReadyEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Warming up user cache...</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findTopActiveUsers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">100</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                // Pre-load frequently accessed users</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">                getUserProfile</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User cache warmed up successfully</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="4-modern-security-patterns">4. Modern Security Patterns</h3>
<p>Implement comprehensive security:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableWebSecurity</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EnableMethodSecurity</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> SecurityConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> JwtAuthenticationEntryPoint</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jwtAuthenticationEntryPoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> JwtAccessDeniedHandler</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jwtAccessDeniedHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> JwtTokenProvider</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jwtTokenProvider</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SecurityFilterChain</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> filterChain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">HttpSecurity</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> http</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> http</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">csrf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">csrf </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> csrf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">disable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">cors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">cors </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cors</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">configurationSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">corsConfigurationSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sessionManagement</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">session </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                session</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sessionCreationPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SessionCreationPolicy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">STATELESS</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">exceptionHandling</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">exceptions </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> exceptions</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticationEntryPoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">jwtAuthenticationEntryPoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">accessDeniedHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">jwtAccessDeniedHandler</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authorizeHttpRequests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/auth/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/public/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">permitAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/users/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasAnyRole</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">USER</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ADMIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasRole</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ADMIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/admin/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasRole</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ADMIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/actuator/health</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/actuator/info</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">permitAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requestMatchers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/actuator/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasRole</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ACTUATOR</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">authenticated</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">oauth2ResourceServer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">oauth2 </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> oauth2</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">jwt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">jwt </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> jwt</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">jwtAuthenticationConverter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">jwtAuthenticationConverter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">jwtDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">jwtDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">addFilterBefore</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">jwtAuthenticationFilter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                UsernamePasswordAuthenticationFilter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">headers</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">headers </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> headers</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">frameOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">deny</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">contentTypeOptions</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">and</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">httpStrictTransportSecurity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">hsts </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> hsts</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">maxAgeInSeconds</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">31536000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">includeSubdomains</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                    .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">preload</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)))</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PasswordEncoder</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> passwordEncoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> BCryptPasswordEncoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">12</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> JwtDecoder</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> jwtDecoder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> JwtDecoders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">fromIssuerLocation</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://your-auth-server.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CorsConfigurationSource</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> corsConfigurationSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CorsConfiguration</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> configuration </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CorsConfiguration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowedOriginPatterns</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://your-frontend-domain.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">https://*.your-domain.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        ));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowedMethods</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">GET</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">POST</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">PUT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">DELETE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">PATCH</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowedHeaders</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">of</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">*</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setAllowCredentials</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">setMaxAge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">3600L</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        UrlBasedCorsConfigurationSource</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> source </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UrlBasedCorsConfigurationSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">registerCorsConfiguration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/**</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> configuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> source</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Method-level security</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Service</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Slf4j</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PreAuthorize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">hasRole('ADMIN') or #id == authentication.principal.id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserResponse</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Method implementation</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PreAuthorize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">@userService.isOwnerOrAdmin(#id, authentication.principal.id)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> deleteUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Method implementation</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> isOwnerOrAdmin</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> currentUserId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">currentUserId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ||</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">               SecurityContextHolder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getContext</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                   .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAuthentication</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                   .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAuthorities</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                   .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stream</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                   .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyMatch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">auth </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> auth</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getAuthority</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">ROLE_ADMIN</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="5-comprehensive-observability">5. Comprehensive Observability</h3>
<p>Implement proper monitoring and metrics:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Configuration</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ConditionalOnProperty</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">management.metrics.enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> havingValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> matchIfMissing</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ObservabilityConfig</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ConditionalOnMissingBean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> TimedAspect</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> timedAspect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">MeterRegistry</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> TimedAspect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> MeterRegistryCustomizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">MeterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> metricsCommonTags</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> registry </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">config</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">commonTags</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">application</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">modern-java-app</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getClass</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getPackage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getImplementationVersion</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">                "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">environment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getCurrentEnvironment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            );</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> HealthIndicator</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> databaseHealthIndicator</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">DataSource</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> dataSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> DataSourceHealthIndicator</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">dataSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">SELECT 1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Bean</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> HealthIndicator</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> customHealthIndicator</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> AbstractHealthIndicator</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">            protected</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> doHealthCheck</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Health</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Builder</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> throws</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Exception</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">                // Custom health checks</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">                boolean</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> externalServiceAvailable </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> checkExternalService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">                if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">externalServiceAvailable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">up</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDetail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">external-service</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Available</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDetail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> else</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                    builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">down</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDetail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">external-service</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Unavailable</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                        .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDetail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">timestamp</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Instant</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        };</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Timed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">api.requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">API request metrics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> MeterRegistry</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">GetMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/users/{id}</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Timed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">api.users.get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5"> description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Get user by ID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PathVariable</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Long</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Counter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Sample</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> sample </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Timer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        try</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">            UserResponse</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">orElseThrow</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> EntityNotFoundException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User not found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">counter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">api.users.get.success</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">EntityNotFoundException</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">counter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">api.users.get.not_found</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> catch</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Exception</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">counter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">api.users.get.error</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> e</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> finally</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            sample</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Timer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">api.users.get.duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User retrieval duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">                .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">register</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PostMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Valid</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestBody</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CreateUserRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        UserResponse</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">counter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">users.created</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">gauge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">users.total</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getTotalUserCount</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">CREATED</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">body</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="which-modern-architecture-patterns-work-best-with-spring-boot">Which modern architecture patterns work best with Spring Boot?</h2>
<h3 id="clean-architecture-with-spring-boot">Clean Architecture with Spring Boot</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Domain layer - Pure business logic</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserId</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Username</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Email</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserStatus</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> status</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> LocalDateTime</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> lastLoginAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">UserId</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Username</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Email</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requireNonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requireNonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Objects</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">requireNonNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">status </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> UserStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ACTIVE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> login</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">status </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> UserStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ACTIVE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserNotActiveException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">User is not active</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">lastLoginAt </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> LocalDateTime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> deactivate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">        this</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">status </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> UserStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">INACTIVE</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> boolean</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> canPerformAction</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Action</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> action</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> status </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> UserStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ACTIVE </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x26;&#x26;</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">               hasPermissionFor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">action</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Domain events</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DomainEvent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getUncommittedEvents</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> List</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">copyOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">uncommittedEvents</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Application layer - Use cases</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UseCase</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Transactional</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CreateUserUseCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> EmailService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> emailService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> DomainEventPublisher</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> eventPublisher</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserId</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> execute</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">CreateUserCommand</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Validate business rules</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">existsByEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> EmailAlreadyExistsException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Email already exists</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">existsByUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            throw</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UsernameAlreadyExistsException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Username already exists</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Create domain object</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">nextId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()),</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> Email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">command</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Persist</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Publish domain events</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        eventPublisher</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">publishAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUncommittedEvents</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Side effects</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        emailService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sendWelcomeEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Infrastructure layer - Framework concerns</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Repository</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> JpaUserRepository</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> SpringDataUserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> springDataRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserMapper</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">UserId</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> springDataRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getValue</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userMapper</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">toDomain</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">User</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        UserEntity</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> entity </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userMapper</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        springDataRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">entity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserId</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> nextId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> UserId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">UUID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">randomUUID</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-do-you-monitor-and-optimize-spring-boot-application-performance">How do you monitor and optimize Spring Boot application performance?</h2>
<h3 id="application-performance-monitoring">Application Performance Monitoring</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequiredArgsConstructor</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Slf4j</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PerformanceMonitor</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> final</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> MeterRegistry</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EventListener</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> handleSlowRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">SlowRequestEvent</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Timer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">Sample</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> sample </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Timer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">start</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">counter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">slow.requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">endpoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEndpoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">method</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        ).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getDuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ></span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 5000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            log</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">warn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Very slow request detected: {} {} took {}ms</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMethod</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEndpoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getDuration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toMillis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">counter</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">very.slow.requests</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">increment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        sample</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">stop</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">Timer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">request.duration</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Request processing time</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">tag</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">endpoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> event</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEndpoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">register</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Scheduled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">fixedRate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 60000</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Every minute</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> reportMetrics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // JVM metrics</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> usedMemory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ManagementFactory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMemoryMXBean</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getHeapMemoryUsage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUsed</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">        long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> maxMemory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ManagementFactory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMemoryMXBean</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getHeapMemoryUsage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getMax</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">gauge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">jvm.memory.used</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> usedMemory</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">gauge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">jvm.memory.usage.percentage</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            (</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">double</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> usedMemory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> maxMemory </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 100</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Connection pool metrics</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        HikariDataSource</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> dataSource </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> getHikariDataSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">dataSource </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">gauge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">connection.pool.active</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                dataSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getHikariPoolMXBean</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getActiveConnections</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            meterRegistry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">gauge</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">connection.pool.idle</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                dataSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getHikariPoolMXBean</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getIdleConnections</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-are-the-best-testing-strategies-for-modern-spring-boot-applications">What are the best testing strategies for modern Spring Boot applications?</h2>
<h3 id="comprehensive-testing-approach">Comprehensive Testing Approach</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Unit Tests with modern techniques</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ExtendWith</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">MockitoExtension</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserServiceTest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Mock</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Mock</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> EmailService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> emailService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">InjectMocks</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserService</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Test</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> shouldCreateUser_WhenValidData</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Given</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CreateUserRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CreateUserRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john_doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">John Doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> savedUser </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">builder</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1L</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john_doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">build</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        when</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">existsByEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">thenReturn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        when</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">existsByUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">anyString</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">())).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">thenReturn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        when</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">any</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">thenReturn</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">savedUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // When</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        UserResponse</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> result </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Then</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isNotNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john_doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">result</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        verify</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">emailService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">sendWelcomeEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        verify</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">argThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user </span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">-></span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john_doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> &#x26;&#x26;</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">equals</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        ));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ParameterizedTest</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ValueSource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">strings</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">""</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">a</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">toolongusernamethatexceedslimit</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> shouldThrowException_WhenInvalidUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Given</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CreateUserRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CreateUserRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">john@example.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">John Doe</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // When &#x26; Then</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThatThrownBy</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(()</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> -></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">createUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isInstanceOf</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">ValidationException</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">hasMessageContaining</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Invalid username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Integration Tests</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">SpringBootTest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">webEnvironment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> SpringBootTest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">WebEnvironment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">RANDOM_PORT</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Testcontainers</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">ActiveProfiles</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserControllerIntegrationTest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Container</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postgres </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> PostgreSQLContainer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;>(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">postgres:15</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withDatabaseName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">testdb</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">            .</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">withPassword</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> TestRestTemplate</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> restTemplate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Autowired</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> UserRepository</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">DynamicPropertySource</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    static</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> configureProperties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">DynamicPropertyRegistry</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">add</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">spring.datasource.url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postgres</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getJdbcUrl</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">add</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">spring.datasource.username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postgres</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        registry</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">add</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">spring.datasource.password</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> postgres</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">::</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getPassword</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Test</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    void</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> shouldCreateAndRetrieveUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Given</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        CreateUserRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> CreateUserRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">integration_test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">test@integration.com</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">Integration Test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // When - Create user</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createResponse </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> restTemplate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">postForEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Then - Verify creation</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">createResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getStatusCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">CREATED</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">createResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getBody</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isNotNull</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Long</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> createResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getBody</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">id</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // When - Retrieve user</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> getResponse </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> restTemplate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getForEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">            "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/api/users/</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">            UserResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        );</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Then - Verify retrieval</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getStatusCode</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">HttpStatus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">OK</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">getResponse</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getBody</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">username</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">integration_test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Verify database state</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        Optional</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userInDb </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> userRepository</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">findById</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userInDb</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isPresent</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">        assertThat</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userInDb</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getUsername</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">isEqualTo</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">integration_test</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="what-deployment-and-production-best-practices-should-you-follow">What deployment and production best practices should you follow?</h2>
<h3 id="docker-configuration">Docker Configuration</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="dockerfile" data-theme="material-theme github-light"><code data-language="dockerfile" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Multi-stage Docker build for optimal image size</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">FROM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> openjdk:21-jdk-slim </span><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> build</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">WORKDIR</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> /app</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">COPY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> . .</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Build application</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ./mvnw clean package -DskipTests</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">FROM</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> openjdk:21-jre-slim</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Create non-root user</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> groupadd -r appuser &#x26;&#x26; useradd -r -g appuser appuser</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Install curl for health checks</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> apt-get update &#x26;&#x26; apt-get install -y curl &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">WORKDIR</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> /app</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Copy application jar</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">COPY</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> --from=build /app/target/*.jar app.jar</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Change ownership to non-root user</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">RUN</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> chown appuser:appuser app.jar</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">USER</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> appuser</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Configure JVM for containerized environments</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">ENV</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> JAVA_OPTS=</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"-XX:+UseContainerSupport \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                -XX:MaxRAMPercentage=75.0 \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                -XX:+UseG1GC \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                -XX:+UseStringDeduplication \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                -XX:+UnlockExperimentalVMOptions \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                -XX:+EnableJVMCI \</span></span>
<span data-line=""><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">                -Dspring.profiles.active=prod"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">EXPOSE</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> 8080</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">HEALTHCHECK</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> --interval=30s --timeout=3s --start-period=60s --retries=3 \</span></span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">    CMD</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> curl -f http://localhost:8080/actuator/health || exit 1</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F78C6C;--shiki-light:#D73A49">ENTRYPOINT</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"sh"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"-c"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">, </span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">"java $JAVA_OPTS -jar app.jar"</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">]</span></span></code></pre></figure>
<h3 id="production-configuration">Production Configuration</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="yaml" data-theme="material-theme github-light"><code data-language="yaml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># application-prod.yml</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">spring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  datasource</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    url</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ${DATABASE_URL}</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    hikari</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      maximum-pool-size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 20</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      minimum-idle</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 5</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      connection-timeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 30000</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      idle-timeout</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 600000</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      max-lifetime</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 1800000</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      leak-detection-threshold</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 60000</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  jpa</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      ddl-auto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> none</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    show-sql</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    properties</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      hibernate</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        dialect</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> org.hibernate.dialect.PostgreSQLDialect</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        jdbc</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          batch_size</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 25</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        order_inserts</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        order_updates</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  cache</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    redis</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      time-to-live</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> 30m</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  security</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    oauth2</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      resourceserver</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        jwt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">          issuer-uri</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> ${JWT_ISSUER_URI}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">logging</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  level</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    com.yourcompany</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> INFO</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    org.springframework.security</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> WARN</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    org.hibernate.SQL</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> WARN</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  pattern</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    console</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">management</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  endpoints</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    web</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      exposure</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        include</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> health,info,metrics,prometheus</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  endpoint</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    health</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      show-details</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62"> when-authorized</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">  metrics</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">    export</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">      prometheus</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#F07178;--shiki-light:#22863A">        enabled</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">:</span><span style="--shiki-dark:#FF9CAC;--shiki-light:#005CC5"> true</span></span></code></pre></figure>
<h2 id="conclusion-and-future-of-java-development">Conclusion and Future of Java Development</h2>
<p>Java continues to evolve rapidly, and staying current with these modern features and best practices is crucial for building maintainable, performant applications. The combination of <strong>Java 21's powerful features</strong> with <strong>Spring Boot's mature ecosystem</strong> provides everything you need to build world-class applications.</p>
<p>For further reading, see the <a href="https://openjdk.org/projects/jdk/21/">JDK 21 Release Notes</a>, the <a href="https://docs.spring.io/spring-boot/docs/3.2.x/reference/htmlsingle/">Spring Boot 3.2 Reference Documentation</a>, and the <a href="https://micrometer.io/docs/observation">Micrometer Observation API</a>.</p>
<h3 id="key-takeaways">Key Takeaways</h3>
<p>Throughout this comprehensive guide, you've learned:</p>
<ol>
<li><strong>Virtual Threads</strong> revolutionize how we handle concurrency in Java applications</li>
<li><strong>Pattern Matching</strong> makes code more readable and less error-prone</li>
<li><strong>Records</strong> are powerful beyond simple DTOs</li>
<li><strong>Spring Boot best practices</strong> that most developers overlook</li>
<li><strong>Modern security patterns</strong> for production applications</li>
<li><strong>Performance optimization</strong> techniques for high-scale applications</li>
<li><strong>Comprehensive testing</strong> strategies for reliable code</li>
<li><strong>Production-ready</strong> deployment configurations</li>
</ol>
<h3 id="whats-coming-next-in-java">What's Coming Next in Java</h3>
<p>Looking ahead to future Java releases, we can expect:</p>
<ul>
<li><strong>Project Loom</strong> enhancements for even better virtual thread performance</li>
<li><strong>Project Panama</strong> for improved native interoperability</li>
<li><strong>Pattern Matching</strong> for switch expressions and instanceof</li>
<li><strong>Value Types</strong> for more efficient object representations</li>
<li><strong>Improved garbage collection</strong> algorithms</li>
</ul>
<h3 id="next-steps-for-your-journey">Next Steps for Your Journey</h3>
<p>To continue improving your Java skills:</p>
<ol>
<li><strong>Experiment</strong> with Virtual Threads in your current projects</li>
<li><strong>Refactor</strong> existing code to use Pattern Matching and Records</li>
<li><strong>Implement</strong> comprehensive observability in your applications</li>
<li><strong>Practice</strong> Test-Driven Development with modern testing tools</li>
<li><strong>Stay updated</strong> with the latest Java Enhancement Proposals (JEPs)</li>
<li><strong>Contribute</strong> to open-source Spring Boot projects</li>
</ol>
<p>The Java ecosystem has never been more exciting, and with these modern techniques, you're well-equipped to build the next generation of high-performance, scalable applications.</p>
<hr>
<p><em>What's your experience with these modern Java features? Have you implemented Virtual Threads in production? Share your thoughts and experiences in the comments below!</em></p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/implementing-outbox-pattern-cdc-microservices">How to Implement the Debezium Outbox Pattern in Spring Boot</a>. The natural next step once your Spring Boot service starts talking to other services: a worked example of the dual-write fix every microservices team eventually needs.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/virtual-threads-java-25">Mastering Virtual Threads in Java 25</a>. The complete guide to the lightweight concurrency model mentioned throughout this post.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-25-compact-object-headers">Java 25 Compact Object Headers</a>. Another Java 25 feature that delivers free performance gains without code changes.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/hibernate-lazy-init-guide">Fixing LazyInitializationException in Spring Boot</a>. Practical Hibernate strategies that complement the Spring Boot best practices covered here.</li>
</ul>
<p><strong>Happy coding with modern Java!</strong> ☕🚀</p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Sanitizer-Lib: The Java Library That Eliminates Input Sanitization Boilerplate Forever]]></title>
      <link>https://www.rabinarayanpatra.com/blogs/sanitizer-lib-intro</link>
      <guid>https://www.rabinarayanpatra.com/blogs/sanitizer-lib-intro</guid>
      <pubDate>Mon, 30 Jun 2025 18:30:00 GMT</pubDate>
      <description><![CDATA[Meet Sanitizer-Lib - a powerful Java library that automatically sanitizes your data with simple annotations. Zero configuration, Spring Boot ready, and enterprise-grade security for modern Java applications.]]></description>
      <content:encoded><![CDATA[<p>Ever tired of writing the same input sanitization code over and over again? Scattered <code>trim()</code>, <code>toLowerCase()</code>, and validation logic across your controllers, services, and entities?</p>
<p><strong>I was too.</strong> That's why I created <strong>Sanitizer-Lib</strong> - a simple, powerful Java library that eliminates sanitization boilerplate with declarative annotations.</p>
<h2 id="what-input-sanitization-problem-does-every-java-developer-face">What input sanitization problem does every Java developer face?</h2>
<p>We've all written code like this:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PostMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestBody</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CreateUserRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Manual sanitization everywhere 😤</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        email </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">trim</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">().</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toLowerCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> firstName </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getFirstName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">firstName </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">!=</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        firstName </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> firstName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">trim</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">        firstName </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> Character</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toUpperCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">firstName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">charAt</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">))</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> +</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                   firstName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">substring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">).</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">toLowerCase</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // ... more boilerplate code</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> firstName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Problems with this approach:</strong></p>
<ul>
<li>🔄 Repetitive boilerplate code</li>
<li>🐛 Easy to forget sanitization in some places</li>
<li>🧪 Hard to test consistently</li>
<li>📈 Maintenance nightmare as the app grows</li>
</ul>
<h2 id="how-does-sanitizer-lib-solve-input-sanitization-with-annotations">How does Sanitizer-Lib solve input sanitization with annotations?</h2>
<p>With Sanitizer-Lib, the same code becomes:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> CreateUserRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> LowerCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TitleCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> firstName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Getters and setters</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PostMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/users</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createUser</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestBody</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> CreateUserRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // Data is automatically sanitized! ✨</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">    User</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> user </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> new</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getEmail</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(),</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">getFirstName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">    return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> ResponseEntity</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">ok</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">userService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">user</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>Clean. Simple. Automatic.</strong></p>
<h2 id="what-key-features-does-sanitizer-lib-offer">What key features does Sanitizer-Lib offer?</h2>
<h3 id="-zero-configuration-setup">🚀 Zero Configuration Setup</h3>
<p>Add the dependency and you're ready to go. Spring Boot auto-configuration handles everything:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.github.rabinarayanpatra.sanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">sanitizer-spring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">1.0.22</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="-built-in-sanitizers-for-common-use-cases">🎯 Built-in Sanitizers for Common Use Cases</h3>
<ul>
<li><strong>TrimSanitizer</strong> - Removes leading/trailing whitespace</li>
<li><strong>LowerCaseSanitizer</strong> - Converts to lowercase (perfect for emails)</li>
<li><strong>TitleCaseSanitizer</strong> - Proper title case formatting</li>
<li><strong>CreditCardMaskSanitizer</strong> - Masks card numbers for security</li>
</ul>
<h3 id="-seamless-spring-boot-integration">🔗 Seamless Spring Boot Integration</h3>
<p>Works automatically with Jackson during JSON deserialization:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// POST /api/users with JSON: {"email": "  JOHN@EXAMPLE.COM  "}</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserDto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> LowerCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Becomes: "john@example.com"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="️-jpa-entity-protection">🗄️ JPA Entity Protection</h3>
<p>Sanitize data before it hits your database:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Entity</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EntityListeners</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SanitizationEntityListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> LowerCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CreditCardMaskSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Automatically masked before save</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-does-sanitizer-lib-work-in-a-real-e-commerce-application">How does Sanitizer-Lib work in a real e-commerce application?</h2>
<p>Here's how Sanitizer-Lib works in a real application:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ProductRequest</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TitleCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> name</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // "  apple iphone  " → "Apple Iphone"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> LowerCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> category</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">    // "  ELECTRONICS  " → "electronics"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> description</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // Preserves case, removes whitespace</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RestController</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ProductController</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">PostMapping</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">/products</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> Product</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> createProduct</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">RequestBody</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> ProductRequest</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // All fields are automatically sanitized during JSON parsing!</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> productService</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">create</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">request</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-do-you-create-custom-sanitizers-for-domain-specific-logic">How do you create custom sanitizers for domain-specific logic?</h2>
<p>Need domain-specific sanitization? Create custom sanitizers easily:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Component</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> PhoneNumberSanitizer</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> FieldSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Override</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1"> sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">String</span><span style="--shiki-dark:#EEFFFF;--shiki-dark-font-style:italic;--shiki-light:#E36209;--shiki-light-font-style:inherit"> input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">input </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">==</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit"> return</span><span style="--shiki-dark:#89DDFF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Remove all non-digits</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#24292E">        String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> digits </span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> input</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">replaceAll</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">[^0-9]</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62"> ""</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">        // Format US phone numbers</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        if</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">digits</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">length</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">()</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> ==</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 10</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">            return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">format</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">(%s) %s-%s</span><span style="--shiki-dark:#89DDFF;--shiki-light:#032F62">"</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                digits</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">substring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                digits</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">substring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">3</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5"> 6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">                digits</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#82AAFF;--shiki-light:#6F42C1">substring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F78C6C;--shiki-light:#005CC5">6</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">        }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-dark-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit">        return</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> digits</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit">// Usage</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> ContactDto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> PhoneNumberSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> phone</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // "555-123-4567" → "(555) 123-4567"</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="how-does-sanitizer-lib-help-with-pci-compliance-and-sensitive-data">How does Sanitizer-Lib help with PCI compliance and sensitive data?</h2>
<p>Handle sensitive data safely with built-in security sanitizers:</p>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Entity</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">@</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">EntityListeners</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">SanitizationEntityListener</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> Payment</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> CreditCardMaskSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cardNumber</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"> // "1234567890123456" → "**** **** **** 3456"</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TitleCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> cardholderName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h2 id="why-did-i-build-sanitizer-lib">Why did I build Sanitizer-Lib?</h2>
<p>As a Java developer, I was frustrated with:</p>
<ol>
<li><strong>Repetitive Code</strong> - Writing the same sanitization logic everywhere</li>
<li><strong>Inconsistency</strong> - Different sanitization rules across the codebase</li>
<li><strong>Maintenance</strong> - Updating sanitization logic in multiple places</li>
<li><strong>Testing</strong> - Ensuring sanitization works correctly everywhere</li>
</ol>
<p>Sanitizer-Lib solves all these problems with a clean, declarative approach that's:</p>
<ul>
<li>✅ <strong>Consistent</strong> - Same sanitization rules everywhere</li>
<li>✅ <strong>Maintainable</strong> - Change logic in one place</li>
<li>✅ <strong>Testable</strong> - Easy to unit test sanitizers</li>
<li>✅ <strong>Performant</strong> - Minimal overhead</li>
<li>✅ <strong>Flexible</strong> - Easy to extend with custom sanitizers</li>
</ul>
<h2 id="architecture-clean-and-modular">Architecture: Clean and Modular</h2>
<p>Sanitizer-Lib follows a modular design:</p>
<pre><code>sanitizer-lib/
├── sanitizer-core/    ← Core API and built-in sanitizers
├── sanitizer-spring/  ← Spring Boot integration
└── sanitizer-jpa/     ← JPA entity lifecycle hooks
</code></pre>
<p>This allows you to use only what you need:</p>
<ul>
<li>Just core? Use <code>sanitizer-core</code></li>
<li>Spring Boot app? Add <code>sanitizer-spring</code></li>
<li>Need JPA integration? Include <code>sanitizer-jpa</code></li>
</ul>
<h2 id="getting-started-in-2-minutes">Getting Started in 2 Minutes</h2>
<h3 id="1-add-the-dependency">1. Add the Dependency</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="xml" data-theme="material-theme github-light"><code data-language="xml" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">io.github.rabinarayanpatra.sanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">groupId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">sanitizer-spring</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">artifactId</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">1.0.15</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">version</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#F07178;--shiki-light:#22863A">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="2-annotate-your-fields">2. Annotate Your Fields</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="java" data-theme="material-theme github-light"><code data-language="java" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">public</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49"> class</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1"> UserRegistrationDto</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> LowerCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> email</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">    @</span><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">Sanitize</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#005CC5">using</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">TrimSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">,</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> TitleCaseSanitizer</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">.</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">class</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">})</span></span>
<span data-line=""><span style="--shiki-dark:#C792EA;--shiki-light:#D73A49">    private</span><span style="--shiki-dark:#C792EA;--shiki-light:#24292E"> String</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E"> fullName</span><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#24292E">}</span></span></code></pre></figure>
<h3 id="3-thats-it">3. That's It!</h3>
<p>Your data is automatically sanitized during JSON deserialization. No configuration needed.</p>
<h2 id="performance-and-production-ready">Performance and Production Ready</h2>
<p>Sanitizer-Lib is built for production use:</p>
<ul>
<li>🚀 <strong>Fast</strong> - Minimal overhead, efficient processing</li>
<li>🔒 <strong>Secure</strong> - Built-in sanitizers for sensitive data</li>
<li>📊 <strong>Observable</strong> - Integrates with Spring Boot Actuator</li>
<li>🧪 <strong>Well-Tested</strong> - Comprehensive test suite</li>
<li>📚 <strong>Documented</strong> - Clear documentation and examples</li>
</ul>
<h2 id="contributing-and-roadmap">Contributing and Roadmap</h2>
<p>Sanitizer-Lib is open source and welcomes contributions!</p>
<p><strong>Current features:</strong></p>
<ul>
<li>✅ Spring Boot integration</li>
<li>✅ JPA entity lifecycle hooks</li>
<li>✅ Built-in sanitizers</li>
<li>✅ Custom sanitizer support</li>
<li>✅ Maven Central deployment</li>
</ul>
<p><strong>Planned features:</strong></p>
<ul>
<li>🔄 Validation integration</li>
<li>🎯 More built-in sanitizers</li>
<li>📈 Performance optimizations</li>
<li>🔧 Configuration properties</li>
</ul>
<h2 id="try-it-today">Try It Today!</h2>
<p>Ready to eliminate sanitization boilerplate from your Java applications?</p>
<h3 id="quick-start">Quick Start:</h3>
<figure data-rehype-pretty-code-figure=""><pre style="--shiki-dark:#EEFFFF;--shiki-light:#24292e;--shiki-dark-bg:#263238;--shiki-light-bg:#fff" tabindex="0" data-language="bash" data-theme="material-theme github-light"><code data-language="bash" data-theme="material-theme github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#546E7A;--shiki-dark-font-style:italic;--shiki-light:#6A737D;--shiki-light-font-style:inherit"># Add to your Spring Boot project</span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">groupId</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">io.github.rabinarayanpatra.sanitizer</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">&#x3C;/groupId></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">artifactId</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">sanitizer-spring</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">&#x3C;/artifactId></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">    &#x3C;</span><span style="--shiki-dark:#FFCB6B;--shiki-light:#6F42C1">version</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">></span><span style="--shiki-dark:#C3E88D;--shiki-light:#032F62">1.0.22</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">&#x3C;/version></span></span>
<span data-line=""><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#EEFFFF;--shiki-light:#24292E">/dependency</span><span style="--shiki-dark:#89DDFF;--shiki-light:#D73A49">></span></span></code></pre></figure>
<h3 id="resources">Resources:</h3>
<ul>
<li>📚 <strong>GitHub</strong>: <a href="https://github.com/rabinarayanpatra/sanitizer-lib">github.com/rabinarayanpatra/sanitizer-lib</a></li>
<li>📦 <strong>Maven Central</strong>: Available now</li>
<li>📖 <strong>Documentation</strong>: Comprehensive examples in the README</li>
<li>🐛 <strong>Issues</strong>: Report bugs and request features on GitHub</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Sanitizer-Lib transforms tedious, error-prone sanitization code into clean, declarative annotations. With zero configuration and powerful Spring Boot integration, it's the missing piece your Java applications need.</p>
<p><strong>Stop writing sanitization boilerplate. Start using Sanitizer-Lib.</strong></p>
<hr>
<p><em>What sanitization challenges are you facing in your Java projects? Try Sanitizer-Lib and let me know how it works for you! ⭐</em></p>
<p>For related reading, see the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html">OWASP Input Validation Cheat Sheet</a>, the <a href="https://github.com/FasterXML/jackson-databind">Jackson Custom Deserializers documentation</a>, and the <a href="https://jakarta.ee/specifications/persistence/3.1/">JPA Entity Lifecycle Events reference</a>.</p>
<h3 id="keep-reading">Keep Reading</h3>
<ul>
<li><a href="https://www.rabinarayanpatra.com/blogs/sanitizer-lib-now-on-maven-central">Sanitizer-Lib is Now Live on Maven Central</a> — The library is now on Maven Central with signed artifacts and zero extra repository config needed.</li>
<li><a href="https://www.rabinarayanpatra.com/blogs/java-libraries-beyond-lombok">10 Essential Java Libraries Beyond Lombok</a> — More libraries that eliminate boilerplate and make your Java code cleaner.</li>
</ul>
<p><strong>Happy coding!</strong> ☕✨</p>]]></content:encoded>
    </item>
  </channel>
</rss>