<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dionisio Cortés Fernández</title>
    <description>The latest articles on DEV Community by Dionisio Cortés Fernández (@dionisioc).</description>
    <link>https://dev.to/dionisioc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F347387%2Fe722eb9b-07dc-4286-9f63-6ce9ecd49db6.jpeg</url>
      <title>DEV Community: Dionisio Cortés Fernández</title>
      <link>https://dev.to/dionisioc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dionisioc"/>
    <language>en</language>
    <item>
      <title>Eight CA Keys Between 1 and 255: What the eMudhra Incident Means for Your Code</title>
      <dc:creator>Dionisio Cortés Fernández</dc:creator>
      <pubDate>Sun, 25 Oct 2026 22:53:55 +0000</pubDate>
      <link>https://dev.to/dionisioc/eight-ca-keys-between-1-and-255-what-the-emudhra-incident-means-for-your-code-4k7o</link>
      <guid>https://dev.to/dionisioc/eight-ca-keys-between-1-and-255-what-the-emudhra-incident-means-for-your-code-4k7o</guid>
      <description>&lt;p&gt;&lt;em&gt;Up to date as of 25 October 2026. The fixes are still changing: on 24 October Oracle shipped a second Java fix (CVE-2026-114820), so guidance written before then calls Java builds fixed that aren't. For anything newer, check &lt;a href="https://emudhra.we-have-a-problem.com/#developers" rel="noopener noreferrer"&gt;the incident site's list of patches&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On 17 October 2026 someone signed the sentence &lt;em&gt;"We have a problem here - Wayne"&lt;/em&gt; with the private keys of eight intermediate certificate authorities: six trusted for TLS by Chrome, Firefox, Safari and Windows, and two trusted by Windows for code signing. Nobody stole those keys. Nobody had to. Each one was an integer between 1 and 255, so anyone could compute it from the public certificate in less time than it takes to read this sentence.&lt;/p&gt;

&lt;p&gt;Chrome and Firefox were protected within hours, Windows and Apple devices within days. Your backend probably wasn't, and the reason says something uncomfortable about TLS outside the browser: &lt;strong&gt;trust doesn't live in the protocol, it lives in a file of root certificates that every runtime picks for itself.&lt;/strong&gt; Patch the OS and you've fixed curl. You haven't fixed Node, a Go binary built last month, or the &lt;code&gt;certifi&lt;/code&gt; inside a virtualenv.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Short answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What was compromised?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 eMudhra intermediate CAs (6 TLS, 2 code signing) and their 8 Sectigo cross-signs (the same keys, also signed by Sectigo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What can an attacker do?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Issue a valid certificate for &lt;em&gt;any&lt;/em&gt; hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What else does the attacker need?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A position on the network path between your client and the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who was exposed?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clients that trust the roots and check neither revocation nor Certificate Transparency: most non-browser TLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What's the fix?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Update the runtime and its trust store, then rebuild everything that embeds them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One chain runs through the whole article, the real one served by the incident's test host &lt;code&gt;dv-g3b-emudhra.we-have-a-problem.com&lt;/code&gt;. We'll look at what a certificate proves, what went wrong with these, what an attacker needs to use them, the before and after on that chain, and how to fix the four runtimes you're most likely to ship: Java, Go, Node.js and Python.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Certificate Actually Proves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition.&lt;/strong&gt; A certificate binds a public key to a name, and a signature from someone else backs that binding. That "someone else" is a certificate authority, whose own certificate is signed by another CA, until you reach a &lt;strong&gt;root&lt;/strong&gt;: a certificate nobody signs, which your client trusts because it's in a file called a &lt;strong&gt;trust store&lt;/strong&gt;. The server's own certificate, the one with the hostname in it, is the &lt;strong&gt;leaf&lt;/strong&gt;. The CAs between the leaf and the root are &lt;strong&gt;intermediates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can print the chain the test host serves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; dv-g3b-emudhra.we-have-a-problem.com:443 &lt;span class="nt"&gt;-showcerts&lt;/span&gt; &amp;lt;/dev/null 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;' s:| i:'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 0 s:CN=dv-g3b-emudhra.we-have-a-problem.com
   i:C=NL, O=eMudhra BV, CN=EM DV TLS CA - G3B
 1 s:C=NL, O=eMudhra BV, CN=EM DV TLS CA - G3B
   i:C=IN, O=eMudhra Technologies Limited, CN=emSign Root TLS CA - G3
 2 s:C=IN, O=eMudhra Technologies Limited, CN=emSign Root TLS CA - G3
   i:C=IN, OU=emSign PKI, O=eMudhra Technologies Limited, CN=emSign ECC Root CA - G3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;s:&lt;/code&gt; is who each certificate speaks for and &lt;code&gt;i:&lt;/code&gt; is who signed it. The server doesn't send the root; the client has to find it in its own trust store. Drawn as a tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dv-g3b-emudhra.we-have-a-problem.com           leaf: "this key speaks for this hostname"
└─ signed by EM DV TLS CA - G3B                intermediate: one of the eight weak keys
   └─ signed by emSign Root TLS CA - G3        intermediate
      └─ signed by emSign ECC Root CA - G3     root: the one in your trust store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the handshake the server sends the chain, and the client checks three things: every signature verifies with the issuer's public key, every certificate is in date and allowed to do what it's doing, and the chain ends at a root &lt;strong&gt;in the client's own trust store&lt;/strong&gt;. The last check is the only place where trust enters the system. Everything else is arithmetic.&lt;/p&gt;

&lt;p&gt;An intermediate's certificate shows how much power it holds. The incident's &lt;a href="https://github.com/galadran/we-have-a-problem.com/tree/main/proof" rel="noopener noreferrer"&gt;proof folder&lt;/a&gt; has all eight; download one and print its limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSLO&lt;/span&gt; https://raw.githubusercontent.com/galadran/we-have-a-problem.com/main/proof/EM-OV-TLS-CA-G3C.crt.pem
openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; EM-OV-TLS-CA-G3C.crt.pem &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-subject&lt;/span&gt; &lt;span class="nt"&gt;-ext&lt;/span&gt; basicConstraints,extendedKeyUsage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;subject=C=US, O=eMudhra Inc, CN=EM OV TLS CA - G3C
X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
    CA:TRUE, pathlen:0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CA:TRUE, pathlen:0&lt;/code&gt; means it may sign leaves but not further CAs. There's no &lt;code&gt;nameConstraints&lt;/code&gt; extension, so nothing limits which hostnames it can back. Whoever holds its private key can sign a certificate for &lt;code&gt;api.your-bank.com&lt;/code&gt;, and your client has no way to tell it from a real one. That's why CA keys live in hardware security modules (HSMs: tamper-resistant devices that never let the key out) and why key ceremonies have witnesses.&lt;/p&gt;

&lt;p&gt;After the fact, two mechanisms can still save a client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Revocation.&lt;/strong&gt; The CA publishes "stop trusting this one" through CRLs (lists of revoked certificates the CA publishes) or OCSP (a service the client asks about one certificate). It works only if the client checks, and most non-browser clients don't by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate Transparency (CT).&lt;/strong&gt; Public, append-only logs where CAs record every certificate they issue, so anyone can see what was issued for a domain. Browsers reject a leaf that hasn't been logged, so a forged certificate either becomes public or gets rejected. Almost nothing outside the browser checks this.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: a certificate is only as trustworthy as the private key of every CA above it, and the trust store is a file &lt;em&gt;your client chooses&lt;/em&gt;, not a property of the internet.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Went Wrong at eMudhra
&lt;/h2&gt;

&lt;p&gt;A P-384 private key is a random 384-bit number &lt;code&gt;k&lt;/code&gt;. The public key is &lt;code&gt;k·G&lt;/code&gt;, the curve's base point added to itself &lt;code&gt;k&lt;/code&gt; times. Going forward is cheap; going back is the problem P-384's security rests on. Mozilla's analysis in the incident report found that the eight keys &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2083449" rel="noopener noreferrer"&gt;were all integers less than 256&lt;/a&gt;: 48 bytes, all zero except the last. With 255 candidates, "going back" means computing 255 points and comparing them with the certificate. One write-up &lt;a href="https://lilting.ch/en/articles/emudhra-weak-intermediate-ca-keys" rel="noopener noreferrer"&gt;measured 28.7 milliseconds&lt;/a&gt; for the full table.&lt;/p&gt;

&lt;p&gt;You don't need to brute-force anything to check it. One of the keys is already public: the key of &lt;code&gt;EM OV TLS CA - G3C&lt;/code&gt; is &lt;strong&gt;4&lt;/strong&gt;. Using the certificate you downloaded above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;x509&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.asymmetric&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ec&lt;/span&gt;

&lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x509&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_pem_x509_certificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EM-OV-TLS-CA-G3C.crt.pem&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;four&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;derive_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SECP384R1&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# the private key "4"
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;public_numbers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;four&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;public_numbers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;P-384 isn't broken. A key of 4 is a &lt;em&gt;valid&lt;/em&gt; key: it parses, it signs, signatures verify. Every format check and every sign-and-verify test passes, which is exactly why nothing caught it for almost eighteen months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it happened&lt;/strong&gt;, according to eMudhra's preliminary explanation in the report: the keys were generated on Utimaco HSMs during a ceremony on 30 April 2025. An approved deviation added a test of key wrap/unwrap (exporting a key encrypted and importing it back), a flaw in that test code created a second, weaker key pair inside the HSM, and because the labels were similar the weak pair was used for the CSRs (certificate signing requests: the public key and name a CA is asked to sign). The root signed them and Sectigo cross-signed them. Other participants in the report point out that an HSM's certified key generation doesn't produce a scalar below 256 on its own, so how that material got there is still an open question. eMudhra hasn't answered yet; it says its full report will come within the 14 days the rules allow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was affected:&lt;/strong&gt; eight intermediates from that ceremony.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Six sign HTTPS certificates:&lt;/strong&gt; &lt;code&gt;EM DV TLS CA&lt;/code&gt;, &lt;code&gt;EM OV TLS CA&lt;/code&gt; and &lt;code&gt;EM EV TLS CA&lt;/code&gt;, each in a G3B and a G3C version. All six chain to &lt;code&gt;emSign ECC Root CA - G3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two sign code:&lt;/strong&gt; &lt;code&gt;EM OV CS CA - G3B&lt;/code&gt; and &lt;code&gt;EM OV CS CA - G3C&lt;/code&gt;. Only Microsoft trusted them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The names follow a pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DV, OV and EV&lt;/strong&gt; say how much the CA checks about a customer before issuing. Domain Validated checks only that they control the domain, Organization Validated also checks that their company exists, and Extended Validation adds stricter legal checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;G3B and G3C&lt;/strong&gt; are two copies of each CA, issued to two eMudhra companies: every G3B to eMudhra BV in the Netherlands, every G3C to eMudhra Inc in the US.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it matters for this incident. The checks are a promise the CA makes, not something the key enforces, so whoever holds any of the six keys can sign for any hostname.&lt;/p&gt;

&lt;p&gt;Sectigo's &lt;code&gt;AAA Certificate Services&lt;/code&gt; root had cross-signed the same eight public keys (signed them too, so they also chain to Sectigo's root), and a cross-sign with the same key is the same compromise. eMudhra itself issued no end-entity certificates (leaves) from them. Whether anyone else did in the eighteen months before the report, nobody can prove either way for clients that don't check CT.&lt;/p&gt;

&lt;p&gt;eMudhra has since &lt;a href="https://emudhra.com/en/press-release/emudhra-to-exit-global-ssl-tls-certificate-business" rel="noopener noreferrer"&gt;announced that it is leaving the public TLS business&lt;/a&gt;, and some trust stores now drop more than the one root behind these keys. Java's second update removes all three eMudhra roots the JDK carried, and certifi 2026.10.23 drops two more (eMudhra Inc's &lt;code&gt;emSign Root CA - C1&lt;/code&gt; and &lt;code&gt;emSign ECC Root CA - C3&lt;/code&gt;). If a server you call still uses an eMudhra certificate, expect it to fail once you update.&lt;/p&gt;

&lt;p&gt;The incident site keeps &lt;a href="https://emudhra.we-have-a-problem.com/#developers" rel="noopener noreferrer"&gt;the authoritative list of who has patched&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: the keys weren't stolen, they were &lt;em&gt;computable&lt;/em&gt;, from public data, by anyone, for almost eighteen months. Treat every client that trusted these roots as if the keys had been public all along, because they effectively were.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why It's Exploitable, and What an Attacker Still Needs
&lt;/h2&gt;

&lt;p&gt;With an intermediate key, an attacker signs a leaf for whatever hostname your service calls. The chain validates. But a certificate alone does nothing: the attacker has to &lt;strong&gt;receive your connection&lt;/strong&gt; to present it. That means a position on the path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS answers they control (poisoning, a compromised resolver, a hijacked record).&lt;/li&gt;
&lt;li&gt;A network device in the middle: a proxy, a router, a hostile Wi-Fi, a compromised egress gateway.&lt;/li&gt;
&lt;li&gt;A routing attack (BGP hijacking: announcing the destination's IP addresses from a network the attacker controls, so traffic goes there).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't remote code execution, and nobody can scan the internet with it. It turns a network position, which TLS normally makes harmless, into full read-and-write access to the traffic.&lt;/p&gt;

&lt;p&gt;So why were browsers safe and your backend not? Because of the two mechanisms above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Revocation.&lt;/strong&gt; eMudhra revoked the intermediates on 18 October. Python, Node, curl and Go on Linux don't check revocation by default, and neither does Java's TLS stack unless you set &lt;code&gt;com.sun.net.ssl.checkRevocation=true&lt;/code&gt;. (Go on macOS and Windows asks the OS verifier, so it picked up Apple's and Microsoft's blocks.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate Transparency.&lt;/strong&gt; No certificate signed by these intermediates appeared in the public CT logs before the block. A browser rejects unlogged certificates, so a forgery would have had to show up in public first. Non-browser clients don't check CT, so for them a forged certificate needs no log entry at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browsers also receive blocklists pushed by the vendor (Chrome's CRLSet, Firefox's OneCRL) without a software update. Your runtime gets nothing until you upgrade it.&lt;/p&gt;

&lt;p&gt;Forged certificates stopped being hypothetical on 20 October. Over the next day and a half, people logged &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2083449#c44" rel="noopener noreferrer"&gt;107,107 of them&lt;/a&gt;, signed with six of the keys, to public CT logs: 106,050 are proof-of-concept certificates for &lt;code&gt;google.com&lt;/code&gt;, and every signature verifies. All but two carry no revocation pointer (the CRL or OCSP address a client would ask), so a client that checks revocation only for the leaf, a common setup, finds nothing to check. Only a client that checks every CA in the chain, receives a pushed blocklist or has dropped the roots rejects them.&lt;/p&gt;

&lt;p&gt;What that means for a typical service:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your code talks to…&lt;/th&gt;
&lt;th&gt;Exposure&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Internal services, with a trust store holding &lt;strong&gt;only&lt;/strong&gt; your private CA&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;The eMudhra roots are never consulted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal services, with the default trust store&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;The attacker must already be inside your network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External APIs, SaaS, your cloud provider's endpoints&lt;/td&gt;
&lt;td&gt;Real, needs a man-in-the-middle&lt;/td&gt;
&lt;td&gt;The path crosses networks you don't control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package registries from CI or laptops&lt;/td&gt;
&lt;td&gt;Unlikely, high impact&lt;/td&gt;
&lt;td&gt;A forged registry can serve a tampered package into every build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browsers&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Pushed blocklists and CT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the second row. "We only call internal services" protects you only if the client &lt;em&gt;trusts&lt;/em&gt; only internal CAs. Most services keep the default trust store and append their private CA to it, so a forged certificate for &lt;code&gt;orders.internal&lt;/code&gt; still validates. It's just much harder to deliver.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: the key is the weapon and the network position is the trigger. Your risk is how many networks you don't control sit between your clients and the servers they call.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;The incident site runs test hosts that serve the compromised chains. Run the check &lt;strong&gt;from where your code runs&lt;/strong&gt;, a pod, a container or a CI job, not from your laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; https://dv-g3b-emudhra.we-have-a-problem.com/      &lt;span class="c"&gt;# eMudhra root&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; https://dv-g3b-aaa-emudhra.we-have-a-problem.com/  &lt;span class="c"&gt;# Sectigo cross-sign&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Before: the client trusts the compromised intermediate
200

# After: trust removed (wording depends on curl's TLS backend; this is OpenSSL)
curl: (60) SSL certificate problem: unable to get local issuer certificate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more hosts serve leaves that were &lt;strong&gt;never logged to CT&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; https://dv-g3b-noct-emudhra.we-have-a-problem.com/
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}\n'&lt;/span&gt; https://dv-g3b-aaa-noct-emudhra.we-have-a-problem.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;200&lt;/code&gt; here is the worst result: your client trusts the compromised intermediates &lt;em&gt;and&lt;/em&gt; doesn't enforce CT, so it could be attacked without anything ever appearing in a public log.&lt;/p&gt;

&lt;p&gt;curl only tests curl's trust store. Your service uses its runtime's, so the &lt;a href="https://github.com/dionisioC/blog/tree/main/posts/2026-10-emudhra-weak-ca-keys/code" rel="noopener noreferrer"&gt;companion script&lt;/a&gt; runs the same request through curl, Python, Node, Java and Go, whichever are installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before you run it at work.&lt;/strong&gt; Each check opens one HTTPS connection to a test host. The &lt;a href="https://emudhra.we-have-a-problem.com/" rel="noopener noreferrer"&gt;incident site&lt;/a&gt; runs them; Oracle and OpenJDK link to it, and its source is &lt;a href="https://github.com/galadran/we-have-a-problem.com" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. The hosts see what any website sees: your public IP, the hostname and the time. curl, Python, Node and Go send a plain &lt;code&gt;GET /&lt;/code&gt;, and the Java probe stops after the TLS handshake. A host your client trusts answers with one line of text, which the scripts throw away. On a company network, three things matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A connection that presents a certificate from a revoked, compromised CA is exactly what a security proxy or intrusion detection system should flag. Tell your security team before you run it.&lt;/li&gt;
&lt;li&gt;If a proxy inspects TLS, your client checks the proxy's certificate instead of the test host's, so the result tells you nothing about your runtime.&lt;/li&gt;
&lt;li&gt;Proxies often block newly registered domains. That shows up as &lt;code&gt;ERROR&lt;/code&gt;, which proves nothing either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can't make the call, the offline check below and &lt;code&gt;Probe --roots&lt;/code&gt; in the Java section need no connection to the test hosts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline, with no network at all.&lt;/strong&gt; The site also publishes &lt;a href="https://github.com/galadran/we-have-a-problem.com/tree/main/chains" rel="noopener noreferrer"&gt;every chain as PEM&lt;/a&gt; (the text format that starts with &lt;code&gt;-----BEGIN CERTIFICATE-----&lt;/code&gt;), so you can check a trust store file directly. Here is the same chain against &lt;code&gt;certifi&lt;/code&gt; before and after the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ openssl verify -CAfile certifi-2026.7.22.pem -untrusted chain.pem chain.pem
chain.pem: OK

$ openssl verify -CAfile certifi-2026.10.19.pem -untrusted chain.pem chain.pem
C = IN, O = eMudhra Technologies Limited, CN = emSign Root TLS CA - G3
error 20 at 2 depth lookup: unable to get local issuer certificate
error chain.pem: verification failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OK&lt;/code&gt; means the bundle still trusts the compromised intermediate. Error 20 at depth 2 means the chain now dead-ends one step before the root: &lt;code&gt;emSign ECC Root CA - G3&lt;/code&gt; is gone from the bundle. The Sectigo chain (&lt;code&gt;dv-g3b-aaa&lt;/code&gt;) fails against &lt;em&gt;both&lt;/em&gt; versions, because &lt;code&gt;certifi&lt;/code&gt; doesn't carry &lt;code&gt;AAA Certificate Services&lt;/code&gt; at all. That cross-sign path matters for stores that do: Windows, the &lt;code&gt;/etc/ssl/cert.pem&lt;/code&gt; macOS ships, and the Linux stores the incident site gives workarounds for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: test with the runtime your service uses, from where it runs. A green curl on your laptop says nothing about a Node container in production.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Every Runtime Needs Its Own Fix
&lt;/h2&gt;

&lt;p&gt;Each runtime answers "which roots do I trust?" differently, so each one got a different fix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Where its roots come from&lt;/th&gt;
&lt;th&gt;What the fix does&lt;/th&gt;
&lt;th&gt;Fixed in&lt;/th&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Java&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The JDK's &lt;code&gt;lib/security/cacerts&lt;/code&gt;; distro packages link it to the OS store&lt;/td&gt;
&lt;td&gt;Distrusts the intermediates through &lt;code&gt;blocked.certs&lt;/code&gt;, then removes four roots from &lt;code&gt;cacerts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;24 Oct updates&lt;/td&gt;
&lt;td&gt;CVE-2026-91403, CVE-2026-114820&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The OS store on Linux; the OS verifier on macOS and Windows&lt;/td&gt;
&lt;td&gt;A hardcoded blocklist in &lt;code&gt;crypto/x509&lt;/code&gt;, &lt;strong&gt;compiled into your binary&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;1.26.10, 1.27.3&lt;/td&gt;
&lt;td&gt;CVE-2026-111943&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node.js&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mozilla's bundle, &lt;strong&gt;compiled into the &lt;code&gt;node&lt;/code&gt; binary&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;NSS 3.131.1 roots, and rejects chains with the blocked public keys&lt;/td&gt;
&lt;td&gt;22.23.4, 24.22.1, 26.12.1&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Python&lt;/strong&gt; (&lt;code&gt;ssl&lt;/code&gt;, &lt;code&gt;urllib&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The OS store&lt;/td&gt;
&lt;td&gt;Whatever your distro's &lt;code&gt;ca-certificates&lt;/code&gt; does&lt;/td&gt;
&lt;td&gt;Your distro's package&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Python&lt;/strong&gt; (&lt;code&gt;requests&lt;/code&gt;, pip, many libs)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;certifi&lt;/code&gt;, a bundle &lt;strong&gt;inside each virtualenv&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Drops &lt;code&gt;emSign ECC Root CA - G3&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;certifi 2026.10.19, pip 26.2.2, uv 0.13.5&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;curl, git, OpenSSL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The OS store&lt;/td&gt;
&lt;td&gt;Drops the root&lt;/td&gt;
&lt;td&gt;e.g. Ubuntu &lt;code&gt;20260816ubuntu0.24.04.1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Go's fix is a list of the eight compromised public keys (their SHA-256 hashes) in &lt;code&gt;crypto/x509&lt;/code&gt;, and &lt;code&gt;Verify&lt;/code&gt; rejects any chain that contains one of them. Blocking by &lt;strong&gt;public key&lt;/strong&gt; rather than by certificate is the right call: the eMudhra certificate and the Sectigo cross-sign carry the same key, so one entry covers both. And because the list is part of the standard library, it ends up inside every binary you build. Upgrading the Go installed on a server changes nothing for a binary that's already compiled: you have to rebuild it with 1.26.10 or 1.27.3.&lt;/p&gt;

&lt;p&gt;Two consequences are easy to miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Update the OS" is not a fix for Node or &lt;code&gt;certifi&lt;/code&gt;.&lt;/strong&gt; Neither reads the OS store by default. For Go on Linux it helps, because it drops the root, but the blocklist only arrives with a rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your CVE scanner won't flag Node or Python.&lt;/strong&gt; Neither fix has a CVE, so a scanner that matches CVEs to versions reports them clean. Check those two by version. For Java, make sure the scanner knows CVE-2026-114820: against the first CVE alone, the 21 October builds look fixed.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: ask of every process, "where does this get its roots, and who updates that file?" The answer is different for each runtime, and sometimes it's "a file compiled into a binary you built last month."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Fixing It, Runtime by Runtime
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Java
&lt;/h3&gt;

&lt;p&gt;Oracle shipped two fixes three days apart, and only the second one is enough. Both score 9.1 and cover Java SE 8, 11, 17, 21, 25 and 27, GraalVM for JDK 17 and 21, and GraalVM Enterprise Edition 21.3.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.oracle.com/security-alerts/alert-cve-2026-91403.html" rel="noopener noreferrer"&gt;CVE-2026-91403&lt;/a&gt;, 21 October: distrusts the eight intermediates and their eight Sectigo cross-signs through &lt;code&gt;blocked.certs&lt;/code&gt;. Oracle's builds 11.0.33.0.2, 17.0.21.0.2, 21.0.13.0.2, 25.0.5.0.2, 27.0.1.0.2 and 8u511 b32.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.oracle.com/security-alerts/alert-cve-2026-114820.html" rel="noopener noreferrer"&gt;CVE-2026-114820&lt;/a&gt;, 24 October: also removes four roots from &lt;code&gt;cacerts&lt;/code&gt;: &lt;code&gt;emSign ECC Root CA - G3&lt;/code&gt;, &lt;code&gt;emSign Root CA - G1&lt;/code&gt;, &lt;code&gt;emSign Root CA - G2&lt;/code&gt; and Sectigo's &lt;code&gt;AAA Certificate Services&lt;/code&gt;. It lists the 21 October builds as affected. Oracle's fixed builds are 11.0.33.0.3, 17.0.21.0.3, 21.0.13.0.3, 25.0.5.0.3, 27.0.1.0.3 and 8u511 b33. Java 8 keeps the update number and bumps the build, so &lt;code&gt;java -version&lt;/code&gt; has to say &lt;code&gt;1.8.0_511-b33&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the first and the last of those roots are behind the compromised chains; the JDK drops eMudhra's other two as well.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update the JDK&lt;/strong&gt; in every place it lives: Docker base images, &lt;code&gt;setup-java&lt;/code&gt; in CI, self-hosted runners, laptops. For Docker, rebuild your own image with &lt;code&gt;docker build --pull&lt;/code&gt; so it starts from the updated base. Redeploying an image you already built, or giving it a new tag, keeps the old JDK inside it. Other vendors (Temurin, Corretto, Zulu and the rest) number their builds differently. OpenJDK's &lt;a href="https://openjdk.org/emudhra" rel="noopener noreferrer"&gt;eMudhra page&lt;/a&gt;, last updated on 24 October, says to look for JDK-8395690 / CVE-2026-114820 in your vendor's release notes, "even if you have updated your runtime in the past few days". It also publishes &lt;a href="https://cr.openjdk.org/~mr/CheckCerts.java" rel="noopener noreferrer"&gt;&lt;code&gt;CheckCerts.java&lt;/code&gt;&lt;/a&gt;, which prints &lt;code&gt;BAD ROOT&lt;/code&gt; for each of the four roots a runtime still trusts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base images lag behind vendor releases.&lt;/strong&gt; On 25 October, three days after Temurin's October builds came out, the official &lt;code&gt;eclipse-temurin:21-jre&lt;/code&gt;, &lt;code&gt;21-jre-alpine&lt;/code&gt; and &lt;code&gt;25-jre-alpine&lt;/code&gt; images still shipped 21.0.12.1 and 25.0.4.1, and &lt;code&gt;Probe&lt;/code&gt; (below) still saw them trust the test hosts. Until the image you build on is updated, &lt;code&gt;--pull&lt;/code&gt; changes nothing: check the result, don't assume it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your vendor hasn't shipped the fix yet, remove the four roots yourself&lt;/strong&gt;, then restart the JVM. OpenJDK's page deletes them by alias, but alias names differ between vendors (Temurin calls the first one &lt;code&gt;cn_emsign_ecc_root_ca__g3,o_emudhra_technologies_limited,ou_emsign_pki,c_in [jdk]&lt;/code&gt;), so find them by subject instead:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  keytool &lt;span class="nt"&gt;-list&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-cacerts&lt;/span&gt; &lt;span class="nt"&gt;-storepass&lt;/span&gt; changeit &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^Alias name: /{a=substr($0,13)} /^Owner: .*CN=(emSign Root CA - G[12]|emSign ECC Root CA - G3|AAA Certificate Services)(,|$)/{print a}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; a&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;keytool &lt;span class="nt"&gt;-delete&lt;/span&gt; &lt;span class="nt"&gt;-cacerts&lt;/span&gt; &lt;span class="nt"&gt;-storepass&lt;/span&gt; changeit &lt;span class="nt"&gt;-alias&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints nothing when it works, and nothing when there's nothing to delete. To see what it will remove, run the first two lines on their own: they print the aliases that match. On &lt;code&gt;eclipse-temurin:21-jre&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  cn_emsign_ecc_root_ca__g3,o_emudhra_technologies_limited,ou_emsign_pki,c_in [jdk]
  cn_emsign_root_ca__g1,o_emudhra_technologies_limited,ou_emsign_pki,c_in [jdk]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run them again afterwards: no output means the roots are gone. Deleting needs write access to &lt;code&gt;cacerts&lt;/code&gt;, so a system JDK needs &lt;code&gt;sudo&lt;/code&gt;; without it, &lt;code&gt;keytool&lt;/code&gt; stops with &lt;code&gt;Permission denied&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;changeit&lt;/code&gt; is the default password of the JDK's &lt;code&gt;cacerts&lt;/code&gt;. Java 8's &lt;code&gt;keytool&lt;/code&gt; has no &lt;code&gt;-cacerts&lt;/code&gt; option, so replace it with &lt;code&gt;-keystore&lt;/code&gt; and the file: &lt;code&gt;$JAVA_HOME/jre/lib/security/cacerts&lt;/code&gt; in a JDK, &lt;code&gt;$JAVA_HOME/lib/security/cacerts&lt;/code&gt; in a JRE. A vendor's &lt;code&gt;cacerts&lt;/code&gt; doesn't always hold all four (Temurin's has &lt;code&gt;emSign Root CA - G1&lt;/code&gt; and &lt;code&gt;emSign ECC Root CA - G3&lt;/code&gt; only), and the command deletes whichever it finds. Whether you update or remove them by hand, servers that chain to &lt;code&gt;AAA Certificate Services&lt;/code&gt; or to eMudhra's other roots stop working, so test the hosts you call before rolling it out.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In a Docker image&lt;/strong&gt;, the keystore is baked into the image, so the removal goes in your Dockerfile, after the &lt;code&gt;FROM&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;  FROM eclipse-temurin:21-jre
  RUN keytool -list -v -cacerts -storepass changeit \
      | awk '/^Alias name: /{a=substr($0,13)} /^Owner: .*CN=(emSign Root CA - G[12]|emSign ECC Root CA - G3|AAA Certificate Services)(,|$)/{print a}' \
      | while IFS= read -r a; do keytool -delete -cacerts -storepass changeit -alias "$a"; done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check an image on your machine, ask the Java inside it. The companion's &lt;a href="https://github.com/dionisioC/blog/tree/main/posts/2026-10-emudhra-weak-ca-keys/code" rel="noopener noreferrer"&gt;&lt;code&gt;Probe.java&lt;/code&gt;&lt;/a&gt; connects to the test hosts through the JVM's own TLS stack, so it sees what your application sees: the image's &lt;code&gt;cacerts&lt;/code&gt;, the &lt;code&gt;blocked.certs&lt;/code&gt; fix and any trust store the image configures. The version string can't tell you that, least of all for a company's own JDK build. Compile it once with &lt;code&gt;javac --release 8 Probe.java&lt;/code&gt;, then run it with the image's Java (8 or later; a JRE is enough):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; java &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;:/probe:ro"&lt;/span&gt; my-image &lt;span class="nt"&gt;-cp&lt;/span&gt; /probe Probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any &lt;code&gt;TRUSTED&lt;/code&gt; line means the image is still exposed. &lt;code&gt;--entrypoint&lt;/code&gt; makes this work even when the image starts your application by default, and settings in its &lt;code&gt;ENV&lt;/code&gt;, such as &lt;code&gt;JAVA_TOOL_OPTIONS&lt;/code&gt;, still apply. If the start script passes &lt;code&gt;-Djavax.net.ssl.trustStore=…&lt;/code&gt; instead, add the same flags after &lt;code&gt;my-image&lt;/code&gt;. On your laptop, &lt;code&gt;java -cp . Probe&lt;/code&gt; does the same for your own JDK.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Company images: only your CAs, or the public roots too?&lt;/strong&gt; The answer tells you which row of the exposure table above you're in. &lt;code&gt;Probe --roots&lt;/code&gt; lists every root the image's Java trusts, and it needs no network:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; java &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;:/probe:ro"&lt;/span&gt; my-image &lt;span class="nt"&gt;-cp&lt;/span&gt; /probe Probe &lt;span class="nt"&gt;--roots&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  /opt/java/openjdk 21.0.12.1+1-LTS, trustStore=/opt/corp/truststore.p12
       CN=ACME Corp Root CA,O=ACME
  1 trusted roots: 0 BAD (behind the compromised chains), 0 DROP (also removed by the 24 October fix)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That image trusts one private root and nothing else. The stock &lt;code&gt;eclipse-temurin:21-jre&lt;/code&gt; lists 118 roots, including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  BAD  CN=emSign ECC Root CA - G3,O=eMudhra Technologies Limited,OU=emSign PKI,C=IN
  DROP CN=emSign Root CA - G1,O=eMudhra Technologies Limited,OU=emSign PKI,C=IN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BAD&lt;/code&gt; marks a root behind the compromised chains. &lt;code&gt;DROP&lt;/code&gt; marks eMudhra's other two roots: they aren't compromised, but the 24 October fix removes them too, and OpenJDK's &lt;code&gt;CheckCerts&lt;/code&gt; flags them as well. Either label makes &lt;code&gt;Probe&lt;/code&gt; exit with 1. On a fixed JDK no line is labelled, the summary reads &lt;code&gt;0 BAD …, 0 DROP …&lt;/code&gt;, and &lt;code&gt;Probe&lt;/code&gt; exits with 0; the number of roots varies by vendor and doesn't matter. Ask Java rather than reading the trust store file with &lt;code&gt;keytool&lt;/code&gt;: a PKCS12 file (the standard keystore format, Java's default since 9) protected by a password lists no entries without that password, so it looks empty when it isn't. Java gets the password from the image's own settings.&lt;/p&gt;

&lt;p&gt;This covers Java only. curl, git, Python's &lt;code&gt;ssl&lt;/code&gt; module and Go read the OS bundle (&lt;code&gt;/etc/ssl/certs/ca-certificates.crt&lt;/code&gt; on Debian, Ubuntu and Alpine), and company images often add their CA to it rather than replace it. &lt;code&gt;docker create&lt;/code&gt; lets you copy it out without starting anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker create my-image&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/etc/ssl/certs/ca-certificates.crt &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  openssl crl2pkcs7 &lt;span class="nt"&gt;-nocrl&lt;/span&gt; &lt;span class="nt"&gt;-certfile&lt;/span&gt; ca-certificates.crt | openssl pkcs7 &lt;span class="nt"&gt;-print_certs&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^subject'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Know which &lt;code&gt;cacerts&lt;/code&gt; you use.&lt;/strong&gt; &lt;code&gt;readlink -f "$JAVA_HOME/lib/security/cacerts"&lt;/code&gt; tells you. JDK packages on Fedora, RHEL, Arch, Gentoo and Oracle Linux read the OS store, so updating &lt;code&gt;ca-certificates&lt;/code&gt; is likely enough there. Homebrew's and FreeBSD's OpenJDK carry their own &lt;code&gt;cacerts&lt;/code&gt;, so upgrade the JDK package itself: &lt;code&gt;brew upgrade openjdk&lt;/code&gt;, not &lt;code&gt;brew update&lt;/code&gt;, which only refreshes Homebrew's list of packages. On 25 October only Homebrew's &lt;code&gt;openjdk&lt;/code&gt; (27.0.1.0.3) had the 24 October fix. &lt;code&gt;openjdk@21&lt;/code&gt; (21.0.13.0.2) still trusted all four roots, and the other versioned packages were at the same or older builds, so remove the roots by hand there. On Debian and Ubuntu, &lt;code&gt;cacerts&lt;/code&gt; points to &lt;code&gt;/etc/ssl/certs/java/cacerts&lt;/code&gt;, and a &lt;a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1069251" rel="noopener noreferrer"&gt;long-standing bug&lt;/a&gt; means updating &lt;code&gt;ca-certificates&lt;/code&gt; doesn't reach it. Regenerate it as root:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/ssl/certs/java/cacerts
  update-ca-certificates &lt;span class="nt"&gt;--fresh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedded JREs.&lt;/strong&gt; Jenkins, Kafka, Elasticsearch, IDEs and many vendor appliances ship their own Java. Updating the system JDK doesn't touch them, so update the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation checking instead of removing the roots.&lt;/strong&gt; OpenJDK and Oracle both accept &lt;code&gt;-Dcom.sun.net.ssl.checkRevocation=true -Dcom.sun.security.enableCRLDP=true&lt;/code&gt; as the alternative. Java checks every certificate in the chain, not just the leaf, so it also rejects the forged certificates that carry no revocation pointer: with those two flags after &lt;code&gt;my-image&lt;/code&gt;, &lt;code&gt;Probe&lt;/code&gt; rejects all four test hosts even on an unpatched &lt;code&gt;eclipse-temurin:21-jre&lt;/code&gt;. It costs you a CRL fetch on new chains and can fail handshakes when a CRL endpoint is unreachable. eMudhra's CRL and OCSP servers have also been &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2085612" rel="noopener noreferrer"&gt;refusing curl's, Wget's and Python's User-Agents&lt;/a&gt; (Java's got through on 25 October), so test it before rolling it out. For signed JARs, JDK 17 and later can check revocation with &lt;code&gt;jarsigner -verify -revcheck -strict app.jar&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you can't update yet&lt;/strong&gt;, Oracle also publishes &lt;a href="https://ops.java/emudhra" rel="noopener noreferrer"&gt;a replacement &lt;code&gt;blocked.certs&lt;/code&gt;&lt;/a&gt; for &lt;code&gt;lib/security/&lt;/code&gt; (on Java 8 the file is &lt;code&gt;blacklisted.certs&lt;/code&gt;). It covers only the first CVE, so remove the roots as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Go
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;See what a binary was built with&lt;/strong&gt;, not what's installed: &lt;code&gt;go version ./bin/my-service&lt;/code&gt; prints the toolchain embedded in the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild with 1.26.10 or 1.27.3&lt;/strong&gt; and redeploy. Pin it where your build reads it: a &lt;code&gt;toolchain go1.26.10&lt;/code&gt; line in &lt;code&gt;go.mod&lt;/code&gt;, &lt;code&gt;go-version&lt;/code&gt; in &lt;code&gt;setup-go&lt;/code&gt;, the &lt;code&gt;golang:&lt;/code&gt; builder image. Go 1.25 and older get no fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild the runtime image too.&lt;/strong&gt; On Linux, Go still reads the OS store, so a &lt;code&gt;distroless&lt;/code&gt; or &lt;code&gt;alpine&lt;/code&gt; base from last month keeps the old roots until you pull a new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check what your &lt;code&gt;node&lt;/code&gt; trusts&lt;/strong&gt;, from the container that runs it:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"const {X509Certificate}=require('crypto');
    const hit=require('tls').rootCertificates.some(p=&amp;gt;new X509Certificate(p).subject.includes('emSign ECC Root CA - G3'));
    console.log(process.version, hit ? 'STILL TRUSTS emSign ECC Root CA - G3' : 'root removed')"&lt;/span&gt;
  &lt;span class="c"&gt;# v22.22.0 STILL TRUSTS emSign ECC Root CA - G3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade to 22.23.4, 24.22.1 or 26.12.1&lt;/strong&gt; everywhere you pin a version: &lt;code&gt;.nvmrc&lt;/code&gt;, &lt;code&gt;engines&lt;/code&gt;, &lt;code&gt;setup-node&lt;/code&gt;, the &lt;code&gt;node:&lt;/code&gt; base image, Lambda runtimes. These releases also fix other CVEs, including a Brotli out-of-bounds write (CVE-2026-111497), so they're worth taking anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 20 and older get nothing.&lt;/strong&gt; Node 20 reached end of life in April 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind the flags.&lt;/strong&gt; &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt; only &lt;em&gt;adds&lt;/em&gt; roots, it never removes one. &lt;code&gt;--use-system-ca&lt;/code&gt; and &lt;code&gt;--use-openssl-ca&lt;/code&gt; switch Node to the OS store, which makes the OS package part of your fix.&lt;/li&gt;
&lt;li&gt;Electron, Bun and Deno bundle their own roots and released their own fixes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check every environment&lt;/strong&gt;, not the interpreter: &lt;code&gt;python -c "import certifi; print(certifi.__version__)"&lt;/code&gt; inside each virtualenv, image and lambda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin &lt;code&gt;certifi&amp;gt;=2026.10.19&lt;/code&gt;&lt;/strong&gt; and regenerate your lockfile so the pin actually lands. Upgrade pip to 26.2.2 or later and uv to 0.13.5 or later. uv ships its own roots, so it needed its own fix: 0.13.4 removed the root and 0.13.5 also rejects chains that carry the compromised keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New Python releases don't fix old virtualenvs.&lt;/strong&gt; 3.15.1, 3.14.9 and 3.13.17 bundle a newer pip for new installs. Existing environments keep whatever &lt;code&gt;certifi&lt;/code&gt; they had.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for vendored bundles.&lt;/strong&gt; Some libraries ship a &lt;code&gt;.pem&lt;/code&gt; inside the package and ignore both the OS and &lt;code&gt;certifi&lt;/code&gt;: &lt;code&gt;find .venv -path '*site-packages*' -name '*.pem'&lt;/code&gt; lists them.&lt;/li&gt;
&lt;li&gt;Code that uses &lt;code&gt;ssl&lt;/code&gt; or &lt;code&gt;urllib&lt;/code&gt; directly follows the OS store, so it's fixed by &lt;code&gt;ca-certificates&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The OS and Your Images
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update &lt;code&gt;ca-certificates&lt;/code&gt;&lt;/strong&gt; on hosts and in images. Patched versions include Debian &lt;code&gt;20250419~deb12u2&lt;/code&gt; / &lt;code&gt;20250419+deb13u1&lt;/code&gt;, Ubuntu &lt;code&gt;20260816ubuntu0.24.04.1&lt;/code&gt; (and its 22.04, 26.04 and 26.10 equivalents), Alpine &lt;code&gt;20261020-r0&lt;/code&gt; and Fedora &lt;code&gt;2026.2.92_v9.0.319&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild, don't just redeploy.&lt;/strong&gt; An image keeps the trust store it was built with. Images pinned by digest or built from a cached layer never pick up the fix. &lt;code&gt;docker build --pull&lt;/code&gt; and check: &lt;code&gt;docker run --rm my-image dpkg -l ca-certificates&lt;/code&gt; (or &lt;code&gt;apk info -v ca-certificates&lt;/code&gt; on Alpine).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS has no &lt;code&gt;ca-certificates&lt;/code&gt; package.&lt;/strong&gt; Safari, Mail and apps built on Apple's frameworks already block these certificates: Apple pushes that change to devices without an OS update. The command-line tools that ship with macOS (curl, git, python3, ruby, perl, openssl) read &lt;code&gt;/etc/ssl/cert.pem&lt;/code&gt; instead, which keeps the root until a future macOS update. &lt;a href="https://support.apple.com/en-us/149555" rel="noopener noreferrer"&gt;Apple's note&lt;/a&gt; and &lt;a href="https://emudhra.we-have-a-problem.com/#workarounds" rel="noopener noreferrer"&gt;the incident site&lt;/a&gt; give the commands to remove it yourself; a macOS update may put it back. The built-in &lt;code&gt;pip3&lt;/code&gt; uses its own, older copy of the roots, and the incident site shows how to point it at the fixed file. Tools installed with Homebrew use Homebrew's own bundle: &lt;code&gt;brew upgrade ca-certificates&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CI/CD and Your Dependencies
&lt;/h3&gt;

&lt;p&gt;The CI risk is a forged registry serving a tampered package. TLS was the only thing standing between your build and that package, unless something else checks the content:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ecosystem&lt;/th&gt;
&lt;th&gt;Content check that survives a broken TLS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go.sum&lt;/code&gt; plus the checksum database, whose signing key ships inside &lt;code&gt;go&lt;/code&gt; itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;integrity&lt;/code&gt; hashes in &lt;code&gt;package-lock.json&lt;/code&gt;, when installing with &lt;code&gt;npm ci&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pip install --require-hashes&lt;/code&gt;, or &lt;code&gt;uv.lock&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle&lt;/td&gt;
&lt;td&gt;Dependency verification metadata (&lt;code&gt;--write-verification-metadata sha256&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maven&lt;/td&gt;
&lt;td&gt;Checksums travel over the same connection and PGP isn't checked by default, so nothing, out of the box&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A job that resolves dependencies from scratch, without a lockfile, trusts TLS and nothing else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managed Runtimes
&lt;/h3&gt;

&lt;p&gt;On AWS Lambda, a function on a managed runtime in &lt;strong&gt;Auto&lt;/strong&gt; update mode gets the patched runtime once AWS publishes one, rolled out in phases. &lt;strong&gt;Function update&lt;/strong&gt; mode only updates when you deploy the function, &lt;strong&gt;container images&lt;/strong&gt; need a rebuild, and anything still on &lt;code&gt;nodejs20.x&lt;/code&gt; won't be patched at all. The &lt;code&gt;INIT_START&lt;/code&gt; line in the function's logs shows the runtime version it started with.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inventory&lt;/strong&gt; every runtime and version per service: Dockerfiles, CI workflows, &lt;code&gt;go.mod&lt;/code&gt;, &lt;code&gt;.nvmrc&lt;/code&gt;, &lt;code&gt;requirements*.txt&lt;/code&gt;, lockfiles, Lambda configs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java&lt;/strong&gt;: JDK at a 24 October build (release notes list JDK-8395690) in images, CI, runners, laptops and embedded JREs, or the four roots removed (or revocation checking on), and &lt;code&gt;Probe&lt;/code&gt; rejecting all four test hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;: every deployed binary rebuilt with 1.26.10 or 1.27.3 (&lt;code&gt;go version ./binary&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: 22.23.4, 24.22.1 or 26.12.1 everywhere; nothing left on Node 20.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt;: &lt;code&gt;certifi&amp;gt;=2026.10.19&lt;/code&gt; in every environment, pip ≥ 26.2.2, uv ≥ 0.13.5, lockfiles regenerated, vendored &lt;code&gt;.pem&lt;/code&gt; files reviewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS&lt;/strong&gt;: &lt;code&gt;ca-certificates&lt;/code&gt; updated on hosts and in base images; images rebuilt with &lt;code&gt;--pull&lt;/code&gt;; on macOS, &lt;code&gt;/etc/ssl/cert.pem&lt;/code&gt; fixed by hand until Apple's update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom trust stores&lt;/strong&gt;: any &lt;code&gt;-Djavax.net.ssl.trustStore&lt;/code&gt;, &lt;code&gt;REQUESTS_CA_BUNDLE&lt;/code&gt;, &lt;code&gt;SSL_CERT_FILE&lt;/code&gt; or bundled &lt;code&gt;.pem&lt;/code&gt; checked for &lt;code&gt;emSign ECC Root CA - G3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party software&lt;/strong&gt;: agents, appliances, backup tools and anything else that makes its own TLS connections, checked against its vendor's notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI&lt;/strong&gt;: dependency verification on (lockfiles with hashes, Gradle verification metadata).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; from where the code runs: the test hosts reject, including the &lt;code&gt;noct&lt;/code&gt; ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't stop at the scanner&lt;/strong&gt;: Node and Python have no CVE for this.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Trust Is a File You Ship
&lt;/h2&gt;

&lt;p&gt;Here's the whole article as two questions to ask of any process that opens a TLS connection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Where does this get its roots?"&lt;/strong&gt; The OS store, a bundle inside the runtime, a package in the virtualenv, a list compiled into the binary. If the answer is "I don't know", that's the finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Who updates that file, and when do I pick it up?"&lt;/strong&gt; Your distro, your runtime's release, your lockfile, your next build. Anything that answers "the next build" is only fixed once you build.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is new. DigiNotar in 2011 and the Symantec distrust in 2018 taught the same lesson, and browsers answered it with pushed blocklists and Certificate Transparency. Most server-side clients got neither. Until they do, the trust store is a dependency like any other: it has versions, it goes stale, and it ships inside your artifacts whether you think about it or not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The check script lives in &lt;a href="https://github.com/dionisioC/blog/tree/main/posts/2026-10-emudhra-weak-ca-keys/code" rel="noopener noreferrer"&gt;the companion folder&lt;/a&gt;: it sends the same request to the four test hosts through curl, Python (&lt;code&gt;ssl&lt;/code&gt; and &lt;code&gt;certifi&lt;/code&gt;), Node, Java and Go, whichever are installed, and prints which ones still trust the compromised chains. The chains and the proof certificates used above come from &lt;a href="https://github.com/galadran/we-have-a-problem.com" rel="noopener noreferrer"&gt;the incident site's repository&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>java</category>
      <category>go</category>
      <category>devops</category>
    </item>
    <item>
      <title>SOLID Without the Acronym: It's Just Cohesion and Coupling</title>
      <dc:creator>Dionisio Cortés Fernández</dc:creator>
      <pubDate>Mon, 14 Sep 2026 20:15:56 +0000</pubDate>
      <link>https://dev.to/dionisioc/solid-without-the-acronym-its-just-cohesion-and-coupling-4e07</link>
      <guid>https://dev.to/dionisioc/solid-without-the-acronym-its-just-cohesion-and-coupling-4e07</guid>
      <description>&lt;p&gt;SOLID isn't really five independent principles. It's mostly two long-standing design ideas, wearing five names.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High cohesion&lt;/strong&gt; — keep the things that change together, together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low coupling&lt;/strong&gt; — depend on stable abstractions, not volatile details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four of the five letters are &lt;em&gt;named consequences&lt;/em&gt; of those two forces, and the fifth comes with an asterisk. Here's the first cut:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Force&lt;/th&gt;
&lt;th&gt;Principles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cohesion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SRP, ISP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coupling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OCP, LSP, DIP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's only a first cut. ISP is contested: the textbook files it under coupling, and its section shows why both readings are right. LSP is the asterisk. It sits under coupling because callers couple to the &lt;em&gt;base contract&lt;/em&gt;, never to your subtype, but it isn't a dial you can turn too far. It's a correctness constraint, which makes it a &lt;em&gt;detector&lt;/em&gt; rather than a design choice.&lt;/p&gt;

&lt;p&gt;Once you see the two forces, you stop memorizing and start deriving, including when &lt;em&gt;not&lt;/em&gt; to apply each letter. Every one of them has a cost, and SOLID applied without judgment produces its own kind of unmaintainable code.&lt;/p&gt;

&lt;p&gt;Every example lives in one system, the checkout slice of a payments product: &lt;code&gt;CheckoutService.checkout(cart)&lt;/code&gt; prices the cart, charges a payment method through a gateway, records the order and returns a result. The domain &lt;em&gt;forces&lt;/em&gt; each principle, and you'll watch them repair each other. For each one: what it means, where it shows up, and what over-applying it costs.&lt;/p&gt;




&lt;h2&gt;
  
  
  S — Single Responsibility
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition.&lt;/strong&gt; A class should have one reason to change. The version that actually helps: &lt;strong&gt;one reason to change means one &lt;em&gt;actor&lt;/em&gt;&lt;/strong&gt;, one group of people who can ask for that change.&lt;/p&gt;

&lt;p&gt;Here the broken version is a &lt;code&gt;CheckoutManager&lt;/code&gt; with &lt;code&gt;total()&lt;/code&gt; and &lt;code&gt;renderReceipt()&lt;/code&gt;. It &lt;em&gt;feels&lt;/em&gt; like one thing ("checkout"), but &lt;code&gt;total()&lt;/code&gt; answers to Finance, which runs a 10% loyalty discount, and &lt;code&gt;renderReceipt()&lt;/code&gt; answers to Marketing, which runs a points program and prints the lines that earned points. Both programs cover the same lines today, so they share one helper. Two actors, two reasons to change, one class: that's the smell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;                             &lt;span class="c1"&gt;// answers to Finance&lt;/span&gt;
        &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;loyaltyDiscount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;renderReceipt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;LoyaltyReceipt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;            &lt;span class="c1"&gt;// answers to Marketing&lt;/span&gt;
        &lt;span class="nc"&gt;LoyaltyReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;rewarded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rewardedItems&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;// "points earned on…"&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;loyaltyDiscount&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;           &lt;span class="c1"&gt;// Finance's discount...&lt;/span&gt;
        &lt;span class="nf"&gt;rewardedItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;                   &lt;span class="c1"&gt;// 10% back on rewarded items&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;rewardedItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;    &lt;span class="c1"&gt;// ...and Marketing's points: one list, the trap&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how it goes wrong. Finance asks you to stop discounting gift wrap. A developer edits &lt;code&gt;rewardedItems()&lt;/code&gt;, the obvious place, and Marketing's receipt silently stops listing gift wrap under "points earned on," which nobody in Marketing asked for. The total moves exactly as Finance wanted (a smaller discount, every item still charged), so the diff looks correct. Review misses it: the class has one name and one obvious topic, but that private helper couples two departments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is a split you already know.&lt;/strong&gt; In a layered app the controller changes with the &lt;em&gt;API shape&lt;/em&gt;, the service with a &lt;em&gt;business rule&lt;/em&gt;, the repository with &lt;em&gt;storage&lt;/em&gt;: three reasons, three classes. Here the same instinct fires &lt;em&gt;inside&lt;/em&gt; the service layer: &lt;code&gt;PriceCalculator&lt;/code&gt; for Finance, &lt;code&gt;ReceiptFormatter&lt;/code&gt; for Marketing, &lt;code&gt;CheckoutService&lt;/code&gt; for the flow.&lt;/p&gt;

&lt;p&gt;Each class gets &lt;em&gt;its own copy&lt;/em&gt; of "which lines count": Finance's skips gift wrap, Marketing's doesn't. DRY says merge them, but they're two actors' rules that merely agreed for a while, and merging them is what caused the bug. Martin calls this &lt;em&gt;accidental duplication&lt;/em&gt;: code that looks the same but changes for different reasons isn't really duplicated. &lt;code&gt;SrpTest&lt;/code&gt; pins both behaviors: the smell's one edit moving discount &lt;em&gt;and&lt;/em&gt; receipt, and the split's two rules disagreeing. Whenever "who asks for changes to this?" gets two answers, you're looking at two classes wearing one name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; Fowler's &lt;em&gt;Refactoring&lt;/em&gt; names both failure modes as smells. Under-apply SRP and you get the god class and its symptom, &lt;strong&gt;divergent change&lt;/strong&gt;: one class edited for many unrelated reasons. Over-apply it and you get &lt;strong&gt;shotgun surgery&lt;/strong&gt;: one logical change spread across ten tiny files, because you scattered things that change together. The dial is &lt;strong&gt;cohesion&lt;/strong&gt;. Split by "these change for different reasons," never by "this method feels different."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: SRP is the &lt;strong&gt;cohesion&lt;/strong&gt; force. Too little separation and you get the god class; too much and you get shotgun surgery. The question is never "how small can this class be," it's "do these parts change for the same reason?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  O — Open/Closed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition.&lt;/strong&gt; A class should be &lt;em&gt;open for extension, closed for modification&lt;/em&gt;: you add behavior by adding a class, not by editing an existing, tested one. The enemy is the &lt;code&gt;if/else&lt;/code&gt; that grows a branch with every new payment method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every new payment method = reopen this function and risk the branches already here.&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"paypal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"bizum"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;- edit working code, again&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polymorphism buys you OCP: depend on an abstraction, and add an &lt;em&gt;implementation&lt;/em&gt; instead of a &lt;em&gt;branch&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// closed&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardPayment&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaypalPayment&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BizumPayment&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;        &lt;span class="c1"&gt;// adding one = a NEW file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New behavior is now a new file, &lt;em&gt;almost&lt;/em&gt;. Something still maps &lt;code&gt;"bizum"&lt;/code&gt; to &lt;code&gt;BizumPayment&lt;/code&gt;, and you'll add that line; here it's &lt;code&gt;PaymentMethodRegistry&lt;/code&gt;, wired in the composition root at the end. OCP doesn't delete the choice, it &lt;em&gt;concentrates&lt;/em&gt; it: out of tested business logic, into one registration line with no logic to break. &lt;em&gt;Closed for modification&lt;/em&gt; never meant "zero edits anywhere," only "no edits where the behavior lives."&lt;/p&gt;

&lt;p&gt;That pays off because the &lt;em&gt;axis of variation&lt;/em&gt; was known: you expected new payment methods. Where the opposite holds, you want the opposite tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The inverse case: closed variation.&lt;/strong&gt; &lt;code&gt;PaymentResult&lt;/code&gt; is what &lt;code&gt;checkout()&lt;/code&gt; and every &lt;code&gt;PaymentMethod.charge()&lt;/code&gt; return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Approved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TxnId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Declined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Timeout&lt;/span&gt;                                   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;   &lt;span class="c1"&gt;// paid already, other terms&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Approved&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Declined&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="nc"&gt;Timeout&lt;/span&gt;     &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Conflict&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;  &lt;span class="c1"&gt;// add a variant → this 'when' stops compiling&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OCP wants a new variant to touch nothing. A sealed type wants a new variant to &lt;em&gt;break every exhaustive &lt;code&gt;when&lt;/code&gt; at compile time&lt;/em&gt;, because for a closed set you own, like an order's states or a payment's outcomes, a silently unhandled case is the bug. Only an exhaustive &lt;code&gt;when&lt;/code&gt; gets that protection: an &lt;code&gt;else&lt;/code&gt; branch opts out, and so does an &lt;code&gt;if (result is Approved)&lt;/code&gt;. That's why &lt;code&gt;CheckoutService&lt;/code&gt;, which decides whether an outcome leaves an order behind, decides with a &lt;code&gt;when&lt;/code&gt;. (Java has the same pair: &lt;code&gt;sealed&lt;/code&gt; types in 17, JEP 409, and the exhaustive pattern &lt;code&gt;switch&lt;/code&gt; in 21, JEP 441.)&lt;/p&gt;

&lt;p&gt;Payment &lt;em&gt;methods&lt;/em&gt; are an open set anyone may extend: OCP and a registry. Payment &lt;em&gt;results&lt;/em&gt; are a closed set you define: a sealed type. One domain, both answers; choosing per axis is the judgment. The trade even has a name, the &lt;em&gt;expression problem&lt;/em&gt; (Philip Wadler, 1998): an open interface makes a new variant cheap and a new operation expensive, because every implementation has to grow the method, and a sealed type flips both. The LSP section shows the expensive direction, when &lt;code&gt;refund&lt;/code&gt; gets bolted onto every &lt;code&gt;PaymentMethod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; OCP up front is indirection on a guess: &lt;strong&gt;premature abstraction (YAGNI)&lt;/strong&gt;. You get an interface with one implementation forever, a plugin system for plugins that never arrive, and readers chasing the interface to find where the work happens. &lt;strong&gt;Wait for the second case&lt;/strong&gt;: that's when OCP starts paying for the indirection instead of just charging you for it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: OCP is a &lt;strong&gt;coupling&lt;/strong&gt; principle. It decouples &lt;em&gt;what varies&lt;/em&gt; (the implementations) from &lt;em&gt;what's stable&lt;/em&gt; (the code that uses them). Add a class, don't edit one. But don't add the interface before the second thing needs it, and when the set is closed, invert the whole idea and let a sealed type break every exhaustive &lt;code&gt;when&lt;/code&gt; on purpose.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  L — Liskov Substitution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition.&lt;/strong&gt; A subtype must be usable anywhere its base type is expected, through a base reference, with no surprises (Liskov &amp;amp; Wing's &lt;em&gt;behavioral subtyping&lt;/em&gt;, 1994). The reframing that matters: &lt;strong&gt;&lt;code&gt;extends&lt;/code&gt; isn't a code-sharing mechanism, it's a published claim&lt;/strong&gt;, "every promise the parent makes, I keep."&lt;/p&gt;

&lt;p&gt;The expensive promises aren't in method signatures. They're properties that hold for an object's whole lifetime, like "balance is never negative" or "the captured amount never exceeds the authorized amount." Callers assume them without checking, which is their whole value and why breaking one costs so much. Broken promises come in two forms, and they fail in opposite ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form 1 — the silent wrong answer.&lt;/strong&gt; Our system can issue store credit (it's where gift-card refunds land, as you'll see):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoreCredit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// the promise: credit &amp;gt;= Money(0), always&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credit&lt;/span&gt;            &lt;span class="c1"&gt;// the promise, observable by every caller&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;topUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="c1"&gt;// every way in guards the promise&lt;/span&gt;
        &lt;span class="n"&gt;credit&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;open&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;redeem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;InsufficientCreditException&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;credit&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VipStoreCredit&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StoreCredit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;       &lt;span class="c1"&gt;// "let VIPs spend past their balance"&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;redeem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="c1"&gt;// same guard as the parent...&lt;/span&gt;
        &lt;span class="n"&gt;credit&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;                     &lt;span class="c1"&gt;// ...but the promise is gone: no exception, just debt&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Callers written against &lt;code&gt;StoreCredit&lt;/code&gt; assume &lt;code&gt;balance()&lt;/code&gt; never goes negative, after &lt;em&gt;any&lt;/em&gt; sequence of calls: reconciliation, the balance the app shows, the liability line Finance reports. Hand them a &lt;code&gt;VipStoreCredit&lt;/code&gt; and they're all wrong at once, with no exception, no crash, and not one changed line of &lt;em&gt;their&lt;/em&gt; code. Quietly incorrect is the expensive kind of wrong.&lt;/p&gt;

&lt;p&gt;The guards matter too. &lt;code&gt;topUp&lt;/code&gt; and &lt;code&gt;redeem&lt;/code&gt; refuse negative amounts, and &lt;code&gt;Money&lt;/code&gt; throws on overflow instead of wrapping. Without them, a plain &lt;code&gt;StoreCredit&lt;/code&gt; could go negative by itself, and the example would blame inheritance for the parent's own bug. The subclass keeps the guard, so breaking the balance promise is the &lt;em&gt;only&lt;/em&gt; thing it does wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form 2 — the loud refusal.&lt;/strong&gt; Checkout grows refunds, and in this product gift cards can't take them (a business rule of this example, not of payments in general). The obvious move widens the strategy for everyone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TxnId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// widened for everyone&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GiftCardPayment&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// fine&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TxnId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;UnsupportedOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gift cards cannot take refunds"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"giftcard"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                            &lt;span class="c1"&gt;// boom — at runtime, in prod, on refund day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type promises something the object refuses to do, and the refusal arrives at runtime instead of compile time. At least it announces itself. Throwing isn't the violation on its own: a contract that allows refusal is kept by refusing. This one promised refunds to every caller, so the refusal breaks it. The fix is to stop claiming the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;RefundableMethod&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TxnId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RefundableMethod&lt;/code&gt; extends &lt;code&gt;PaymentMethod&lt;/code&gt; in the only safe direction: a method that can also refund keeps every promise a charge-only view makes, never the reverse. Gift cards implement only &lt;code&gt;PaymentMethod&lt;/code&gt;, so there's no &lt;code&gt;refund&lt;/code&gt; on them to call and nothing to throw.&lt;/p&gt;

&lt;p&gt;In the repo, that fix carries real weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refunds enter by order.&lt;/strong&gt; The order knows which method took the money and which transaction to reverse, so nobody can pair a card's refund with a gift card's charge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The capability is asked, not assumed.&lt;/strong&gt; Methods arrive by name, so one question stays at runtime: can this one refund? &lt;code&gt;PaymentMethod.refundable()&lt;/code&gt; promises only an answer, and &lt;code&gt;null&lt;/code&gt; keeps that promise; &lt;code&gt;RefundableMethod&lt;/code&gt; answers with itself. That names a contract, not a class, unlike the &lt;code&gt;is GiftCardPayment&lt;/code&gt; patch below, so a new refundable method touches nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It survives wrappers.&lt;/strong&gt; An &lt;code&gt;as? RefundableMethod&lt;/code&gt; stops at a &lt;code&gt;PaymentMethod by inner&lt;/code&gt; wrapper, quietly turning every card refund into store credit. &lt;code&gt;by&lt;/code&gt; forwards the question to the card inside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gift cards settle in store credit.&lt;/strong&gt; A gift-card order comes back &lt;code&gt;NotRefundable&lt;/code&gt;, and &lt;code&gt;SupportCreditFlow&lt;/code&gt; issues store credit instead: the class whose promise you just watched a subclass break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each order settles once.&lt;/strong&gt; The order records its settlement, so a second press of the refund button answers &lt;code&gt;AlreadySettled&lt;/code&gt; and moves no money.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What repaired the broken contract was &lt;strong&gt;segregating the interface&lt;/strong&gt;, which happens to be the next letter. The principles aren't five separate rules; they repair each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; LSP isn't a dial: nothing is "too substitutable," and it can only be kept or broken. Keeping it has a price, and the price lives in the contract. You can weaken the base contract until every subtype can keep it, as &lt;code&gt;java.util.Collection&lt;/code&gt; does: its Javadoc marks &lt;code&gt;add&lt;/code&gt; and &lt;code&gt;remove&lt;/code&gt; as optional operations that may throw &lt;code&gt;UnsupportedOperationException&lt;/code&gt;, so an unmodifiable list keeps the contract by refusing, and every caller handles a refusal the type allows. Or you keep the contract strong and split it, as &lt;code&gt;RefundableMethod&lt;/code&gt; did, and pay in interfaces: ISP's explosion. Weaker promises or more types: that's the real dial.&lt;/p&gt;

&lt;p&gt;What LSP rules out is the third option, patching the caller. &lt;code&gt;if (method is GiftCardPayment) skipRefund()&lt;/code&gt; fixes the wrong answer by breaking OCP, so now two principles are broken instead of one. That's LSP's real job in your toolbox: it detects bad inheritance. When a tempting IS-A can't honor the full contract, stop inheriting. Narrow the contract until every implementation can keep it, or hold the object in a field instead of extending it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: LSP is a &lt;strong&gt;coupling&lt;/strong&gt; principle. Callers couple to the &lt;em&gt;base contract&lt;/em&gt;, and every subtype must be safe behind it. No surprises through a base reference. It's not a dial, it's a detector: when IS-A can't keep the contract, don't inherit. The dial it leaves you is in the contract itself: weaker promises or more types.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  I — Interface Segregation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition.&lt;/strong&gt; No client should be forced to depend on methods it doesn't use. The key word is &lt;strong&gt;client&lt;/strong&gt;: you segregate by &lt;em&gt;role&lt;/em&gt;, one interface per &lt;em&gt;kind of caller&lt;/em&gt;, not by chopping an interface into pieces. Ask "who calls this, and which slice do they actually need?", never "how many methods is too many?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One implementation may serve every role...&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StripePaymentGateway&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PaymentReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ...but each client sees only the contract its role needs.&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                        &lt;span class="c1"&gt;// the role that moves money&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChargeRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TxnId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PaymentReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                         &lt;span class="c1"&gt;// the role that looks at it&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DateRange&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Txn&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StatementsScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentReader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                &lt;span class="c1"&gt;// can't move money&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RefundableMethod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// can't read statements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation didn't split; the &lt;em&gt;view&lt;/em&gt; of it did, and the benefits are concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The statements screen has no &lt;code&gt;charge&lt;/code&gt; or &lt;code&gt;refund&lt;/code&gt; in scope, so it can't move money by accident. That narrows access rather than proving it (a cast could still reach the other role of the same object), but least privilege by default is what a payments audit asks for.&lt;/li&gt;
&lt;li&gt;A change to a charging signature no longer touches any read-only client.&lt;/li&gt;
&lt;li&gt;The test double for &lt;code&gt;StatementsScreen&lt;/code&gt; stubs one query method instead of a whole PSP (payment service provider).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system has now segregated twice, on two different questions: &lt;code&gt;RefundableMethod&lt;/code&gt; by &lt;em&gt;what an implementation can truly promise&lt;/em&gt;, this split by &lt;em&gt;the role a client plays&lt;/em&gt;. They compose. &lt;code&gt;RefundFlow&lt;/code&gt; decides a refund is allowed (capability, on the domain method), then &lt;code&gt;CardPayment&lt;/code&gt; carries it out through &lt;code&gt;PaymentGateway.refund&lt;/code&gt; (mechanism, on the port). No client gets the raw port: &lt;code&gt;RefundHandler&lt;/code&gt;, the support desk's refund button, holds &lt;code&gt;RefundFlow&lt;/code&gt;. Handed &lt;code&gt;PaymentGateway&lt;/code&gt; instead, it could refund any transaction, gift cards included, without asking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The symptom to look for.&lt;/strong&gt; An adapter full of no-ops means the interface above it was never cut by role. The repo has a quiet version: the checkout tests' fake gateways only ever charge, yet each one stubs &lt;code&gt;refund&lt;/code&gt; as a no-op. One stub per fake is the cheap end of the dial, and splitting &lt;code&gt;PaymentGateway&lt;/code&gt; over it would be the explosion described next. When the fakes stub three or four methods, cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; Over-apply ISP and you get &lt;strong&gt;interface explosion&lt;/strong&gt;: a hundred one-method interfaces, every call site holding a different name for the same object, and nobody able to say what the thing &lt;em&gt;is&lt;/em&gt; anymore. That's SRP's failure pair one level up: the fat interface is the god class of contracts, and fragmentation is their shotgun surgery. ISP &lt;em&gt;is&lt;/em&gt; SRP applied to interfaces, with the same dial: segregate by the client roles that &lt;em&gt;actually exist&lt;/em&gt;, not by method count. Two roles mean two interfaces. Five methods don't mean five interfaces.&lt;/p&gt;

&lt;p&gt;The textbook files ISP under &lt;strong&gt;coupling&lt;/strong&gt;, and Robert C. Martin's own formulation backs it: "clients should not be forced to depend upon interfaces that they do not use" (&lt;em&gt;The C++ Report&lt;/em&gt;, 1996), because forcing them "results in an inadvertent coupling between all the clients." Both framings are right; they answer different questions. What segregation &lt;em&gt;buys&lt;/em&gt; is decoupling. What tells you &lt;em&gt;where to cut&lt;/em&gt; is cohesion: the roles whose methods change together.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: ISP is the &lt;strong&gt;cohesion&lt;/strong&gt; force applied to contracts. Split by caller, not by method. Too few cuts and you get the fat interface; too many and you get interface explosion; the dial is the roles that actually exist.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  D — Dependency Inversion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition.&lt;/strong&gt; The original formulation has two halves: &lt;em&gt;high-level modules should not depend on low-level modules — both should depend on abstractions; and abstractions should not depend on details — details should depend on abstractions.&lt;/em&gt; What gets inverted isn't "now there's an interface." It's &lt;strong&gt;ownership&lt;/strong&gt;: the high-level policy &lt;em&gt;owns&lt;/em&gt; the abstraction, and the low-level detail &lt;em&gt;implements&lt;/em&gt; it. The test is a single question: &lt;strong&gt;which module declares the interface?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// module: domain — the high-level policy OWNS the ports.&lt;/span&gt;
&lt;span class="c1"&gt;// (PaymentGateway and PaymentReader from the last section live here too.)&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                      &lt;span class="c1"&gt;// written in the domain's vocabulary,&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;                &lt;span class="c1"&gt;// living in the domain's module&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PriceCalculator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethodRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// business rules; zero infra imports&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// module: infrastructure — depends on domain; domain has never heard of it&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StripePaymentGateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StripeClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PaymentReader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DynamoOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DynamoDbClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the compile-time arrow: &lt;code&gt;infrastructure&lt;/code&gt; imports &lt;code&gt;domain&lt;/code&gt;, so the domain compiles alone, with no Stripe SDK and no AWS on its classpath. That inward arrow is the dependency rule of hexagonal architecture (ports and adapters): the domain owns the port, and infrastructure provides the adapter. Hexagonal adds more than the arrow, such as an explicit application boundary and adapters on both the driving and the driven side, but the arrow itself is DIP at the module boundary. Here it is as the repo's actual layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checkout/
  domain/            # no infra imports; a test enforces it
    Money  Cart  Order  Receipt  PaymentResult (sealed)  RefundResult (sealed)
    PaymentGateway  PaymentReader  OrderRepository  Clock      &amp;lt;- ports
    PaymentMethod / RefundableMethod (Card, Paypal, Bizum; GiftCard is charge-only)
    PaymentMethodRegistry  RefundFlow  PriceCalculator  ReceiptFormatter  CheckoutService
    StoreCredit  SupportCreditFlow                             &amp;lt;- where gift-card refunds land
  infrastructure/    # depends on domain; domain has never heard of it
    StripePaymentGateway  StripeClient  InMemoryOrderRepository  Meter  KeyStore
    RetryingGateway  MeteredGateway  IdempotentGateway         &amp;lt;- decorators ('by')
  clients/           # also depends on domain: the callers, not the adapters
    StatementsScreen                                           &amp;lt;- ISP's read-only role view
    RefundHandler                                              &amp;lt;- holds RefundFlow, never the port
  smells/            # the broken examples, compiling, each pinned by a test
    CheckoutManager  VipStoreCredit
  app/
    Main.kt          # the composition root: wires everything; DIP with no framework
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two honest caveats. First, so &lt;code&gt;main&lt;/code&gt; runs anywhere with zero credentials, the adapters are an &lt;code&gt;InMemoryOrderRepository&lt;/code&gt; and a no-network &lt;code&gt;StripeClient&lt;/code&gt; stand-in, not the real Dynamo and Stripe SDKs. The ports can't tell the difference, and that a database can become a map in one line of wiring is DIP's whole claim.&lt;/p&gt;

&lt;p&gt;Second, &lt;code&gt;domain&lt;/code&gt; and &lt;code&gt;infrastructure&lt;/code&gt; are packages in one Gradle project, so the compiler alone wouldn't stop a domain file from importing an adapter. &lt;code&gt;ArchitectureTest&lt;/code&gt; does, with two checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Imports.&lt;/strong&gt; A &lt;code&gt;domain/&lt;/code&gt; file may import, or name in full, only the domain, Kotlin, and the JDK's &lt;code&gt;java.time&lt;/code&gt; and &lt;code&gt;java.util&lt;/code&gt;. The JDK allowance is narrow on purpose: &lt;code&gt;java.sql&lt;/code&gt; and &lt;code&gt;java.net.http&lt;/code&gt; ship with the JDK too, and they're infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ambient reads.&lt;/strong&gt; &lt;code&gt;java.lang&lt;/code&gt; needs no import, and allowing &lt;code&gt;java.time&lt;/code&gt; for &lt;code&gt;Instant&lt;/code&gt; also allows &lt;code&gt;Instant.now()&lt;/code&gt;, exactly the read the &lt;code&gt;Clock&lt;/code&gt; port exists to replace. This check fails on the system clock, &lt;code&gt;System&lt;/code&gt;, &lt;code&gt;Runtime&lt;/code&gt;, &lt;code&gt;Thread&lt;/code&gt; or &lt;code&gt;ProcessBuilder&lt;/code&gt; anywhere in domain code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are text scans: a tripwire, not a proof. In a production codebase, make them Gradle subprojects and the build enforces the arrow for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gotcha: DI != DIP.&lt;/strong&gt; Dependency &lt;em&gt;injection&lt;/em&gt; is a mechanism: someone hands objects their collaborators. Dependency &lt;em&gt;inversion&lt;/em&gt; is a principle about who owns the abstraction, and you can have either without the other. &lt;code&gt;@Autowired StripePaymentGateway&lt;/code&gt;, the concrete class, is DI with zero DIP: a framework injecting your coupling for you. Hand-wiring in &lt;code&gt;main&lt;/code&gt; is DI in its purest form, and DIP too, because the domain owns the interfaces being wired. Depend on an interface your own module owns and you have DIP, container or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The payoff.&lt;/strong&gt; Testability, with cause and effect in the right order. You can hand &lt;code&gt;CheckoutService&lt;/code&gt; a fake gateway and an in-memory &lt;code&gt;OrderRepository&lt;/code&gt; &lt;em&gt;because&lt;/em&gt; it depends on abstractions the domain owns. The mock isn't the point; it's the &lt;em&gt;evidence&lt;/em&gt;. If you can't test a class without booting the database, DIP is telling you an arrow points the wrong way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; The degenerate form is &lt;strong&gt;interface-for-everything&lt;/strong&gt;: &lt;code&gt;FooService&lt;/code&gt;/&lt;code&gt;FooServiceImpl&lt;/code&gt; pairs that exist because "we always do it that way," OCP's premature abstraction moved up a layer. Abstract at &lt;strong&gt;true frontiers&lt;/strong&gt;, the I/O boundaries (the database, HTTP, queues, the clock, someone else's SDK) where a second implementation genuinely exists: the real one and the test fake, at minimum. An interface between two classes in the same package that always change together isn't low coupling; it's low cohesion disguised as low coupling.&lt;/p&gt;

&lt;p&gt;Every port here sits on such a frontier, &lt;code&gt;Clock&lt;/code&gt; included. It's deliberately narrower than &lt;code&gt;java.time.Clock&lt;/code&gt;, which is abstract, carries a time zone the domain never reads, and no lambda can implement. The JDK's &lt;code&gt;java.time.InstantSource&lt;/code&gt; (Java 17) has the right shape (one method, no zone, lambda-friendly) and would do; the domain declares its own anyway, like every other port, so the policy states its need in its own words. Three lines is the whole price.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you remember one thing: DIP is the &lt;strong&gt;coupling&lt;/strong&gt; principle at architecture scale. The domain owns the interface, details implement it, arrows point inward. DI is a mechanism; DIP is a direction. Abstract at real frontiers, not everywhere.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Composition Root
&lt;/h2&gt;

&lt;p&gt;Every abstraction has to become an object somewhere, and in this system exactly one place gets to do it: &lt;code&gt;Main.kt&lt;/code&gt;, the file where all five principles stop being prose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Main.kt&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;meter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Meter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// the domain's own port; java.time.Clock never leaks inward&lt;/span&gt;

    &lt;span class="c1"&gt;// ISP: one Stripe adapter, two roles. The money-moving role gets the decorator stack below;&lt;/span&gt;
    &lt;span class="c1"&gt;// the read-only role gets the adapter itself, since a statement needs no retries and no keys.&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stripe&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StripePaymentGateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StripeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;// Composition: cross-cutting concerns as a decorator stack. The ORDER is a&lt;/span&gt;
    &lt;span class="c1"&gt;// decision, and it lives here, in wiring, not in a class hierarchy.&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;MeteredGateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;RetryingGateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;IdempotentGateway&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;KeyStore&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// OCP's real cost, concentrated: one plain line per payment method.&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;methods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaymentMethodRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"card"&lt;/span&gt; &lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;CardPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"paypal"&lt;/span&gt; &lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;PaypalPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"bizum"&lt;/span&gt; &lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;BizumPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"giftcard"&lt;/span&gt; &lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;GiftCardPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// claims no refund capability&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// DIP: details handed to a domain that has never heard of them.&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryOrderRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// prod: DynamoOrderRepository, same port, one line&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;checkout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CheckoutService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;PriceCalculator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;                   &lt;span class="c1"&gt;// SRP: Finance's class, alone&lt;/span&gt;
        &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                               &lt;span class="c1"&gt;// the same clock the PSP stand-in stamps with&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// The clients, each holding only the role it plays.&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;statements&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StatementsScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;// ISP: reads, can't move money&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;refundDesk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RefundHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RefundFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// LSP: refunds only through the gate&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;storeCredit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SupportCreditFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoreCredit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;            &lt;span class="c1"&gt;// where a gift card's refund lands&lt;/span&gt;

    &lt;span class="c1"&gt;// …then a short demo script plays a customer and the support desk: a card order and a&lt;/span&gt;
    &lt;span class="c1"&gt;// gift-card order, the statement, both refunds, and the card's refund button pressed twice.&lt;/span&gt;
    &lt;span class="nf"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refundDesk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storeCredit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"logical charges metered: ${meter.count("&lt;/span&gt;&lt;span class="n"&gt;charges&lt;/span&gt;&lt;span class="s"&gt;")}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as a checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Composition.&lt;/strong&gt; The gateway is wrapped three times: &lt;code&gt;IdempotentGateway&lt;/code&gt; so an approved order isn't charged again, &lt;code&gt;RetryingGateway&lt;/code&gt; so a timed-out call gets another try, and &lt;code&gt;MeteredGateway&lt;/code&gt; so someone can count what happened. Each wrapper holds the &lt;em&gt;port&lt;/em&gt;, not a concrete class, which is why they stack at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order.&lt;/strong&gt; Where each wrapper sits is a decision made here. &lt;code&gt;MeteredGateway(RetryingGateway(…))&lt;/code&gt; counts &lt;em&gt;logical&lt;/em&gt; charges; &lt;code&gt;RetryingGateway(MeteredGateway(…))&lt;/code&gt; counts &lt;em&gt;attempts&lt;/em&gt;. Neither is wrong; they're different metrics, a one-line diff apart. Neither counts PSP calls, though: in both, idempotency sits inside the meter, so a double-click the cache answers still gets metered. To count PSP calls, wrap the Stripe adapter itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCP.&lt;/strong&gt; The registry: the one registration line the OCP section promised you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ISP.&lt;/strong&gt; One Stripe adapter, two roles: &lt;code&gt;CardPayment&lt;/code&gt; sees a decorated &lt;code&gt;PaymentGateway&lt;/code&gt;, &lt;code&gt;StatementsScreen&lt;/code&gt; an undecorated &lt;code&gt;PaymentReader&lt;/code&gt;, since a statement needs no retries or keys. Cross-cutting concerns attach per role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LSP.&lt;/strong&gt; Details arrive typed as ports (&lt;code&gt;PaymentGateway&lt;/code&gt;, not &lt;code&gt;StripePaymentGateway&lt;/code&gt;), and refunds reach money only through &lt;code&gt;RefundFlow&lt;/code&gt;, which asks each method for the capability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DIP.&lt;/strong&gt; No framework in sight: DI in its purest form, down to one clock shared by the domain and the PSP stand-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SRP.&lt;/strong&gt; &lt;code&gt;main&lt;/code&gt; holds no business logic, so its single reason to change is "the wiring changed." The demo script lives in a function of its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decorators forward with Kotlin's &lt;code&gt;by inner&lt;/code&gt;: delegate the whole port to the wrapped object, then override only what you care about. That's black-box reuse with no fragile base class, at one cost: &lt;code&gt;by&lt;/code&gt; forwards whatever it isn't told about. &lt;code&gt;refund&lt;/code&gt; already passes through all three layers unmetered, unretried and without a key, and a method added to &lt;code&gt;PaymentGateway&lt;/code&gt; tomorrow would slip through the same way, silently. It's the open default, the opposite of a sealed &lt;code&gt;when&lt;/code&gt; that makes you decide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the idempotency layer can't do.&lt;/strong&gt; It's a local memory of approvals for calls made one at a time. If the PSP approves but its response never arrives, or two calls race past the cache, only the key sent to the PSP protects you, which is why the Stripe adapter forwards it. Both layers also check what a key is reused &lt;em&gt;for&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same request:&lt;/strong&gt; the original answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same order, another amount or method:&lt;/strong&gt; &lt;code&gt;Conflict&lt;/code&gt;, the way &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe refuses a key reused with different parameters&lt;/a&gt;. Otherwise a cart edited after payment would come back "approved" at a total nobody charged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why not &lt;code&gt;Declined&lt;/code&gt;:&lt;/strong&gt; a decline means no money moved, and here some may have. If the PSP charged the first attempt but its response never arrived, a charge exists that no order records, and "declined" would tell a customer who paid to pay again. Finding that charge by its key is reconciliation, out of scope here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same price, other lines:&lt;/strong&gt; the PSP only sees amount and method, so swapping size M for L replays cleanly through both layers. Only the domain knows the lines, so &lt;code&gt;CheckoutService&lt;/code&gt; compares them with the recorded order and answers &lt;code&gt;Conflict&lt;/code&gt; too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keying the order has one more price: a real PSP replays the &lt;em&gt;first&lt;/em&gt; answer for a key, declines included, so paying another way after a decline needs a new key (order plus attempt number), which the sample leaves out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the retry layer won't do.&lt;/strong&gt; It retries only &lt;code&gt;Timeout&lt;/code&gt;, through an exhaustive &lt;code&gt;when&lt;/code&gt;: a decline is an answer, not a failure, and asking again only asks the same question. It also retries only a request that carries a key. A &lt;code&gt;Timeout&lt;/code&gt; means nobody knows whether the PSP charged, so a keyed retry is safe (the PSP dedupes it) and a keyless one could charge twice. The wired flow always has a key, because an &lt;code&gt;OrderId&lt;/code&gt; can't be blank. Add another &lt;code&gt;PaymentResult&lt;/code&gt; and that &lt;code&gt;when&lt;/code&gt; stops compiling until someone decides whether the new outcome is worth retrying.&lt;/p&gt;




&lt;h2&gt;
  
  
  Throw Away the Acronym
&lt;/h2&gt;

&lt;p&gt;Here's the whole article as two questions, the two to actually ask in code review:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Do these things change for the same reason?"&lt;/strong&gt; — the &lt;em&gt;cohesion&lt;/em&gt; question. If yes, keep them together; if no, separate them. SRP asks it about classes, ISP about interfaces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"If this changes, what else is forced to move?"&lt;/strong&gt; — the &lt;em&gt;coupling&lt;/em&gt; question. OCP asks it about new features (nothing should move: add a class), LSP about subtypes (callers of the base must never notice), DIP about architecture (details move; policy doesn't).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The two feed each other: group what changes together and fewer changes cross a module line, so coupling falls; cut a dependency and each side comes out more focused, so cohesion rises.&lt;/p&gt;

&lt;p&gt;One bounded context was enough for all five letters, because the domain forced each one: pricing and receipt copy answer to different departments, new payment methods keep arriving, gift cards can't refund, a statements screen has no business moving money, and checkout has to be testable without a PSP. Here is every dial in one place:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principle&lt;/th&gt;
&lt;th&gt;Under-applied&lt;/th&gt;
&lt;th&gt;Over-applied&lt;/th&gt;
&lt;th&gt;The dial&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SRP&lt;/td&gt;
&lt;td&gt;God class&lt;/td&gt;
&lt;td&gt;Shotgun surgery&lt;/td&gt;
&lt;td&gt;One actor per class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCP&lt;/td&gt;
&lt;td&gt;Growing &lt;code&gt;if/else&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Speculative interfaces&lt;/td&gt;
&lt;td&gt;Wait for the second case; seal what you own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LSP&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Broken:&lt;/em&gt; &lt;code&gt;is&lt;/code&gt;-check patches in callers&lt;/td&gt;
&lt;td&gt;— (constraint, not a dial)&lt;/td&gt;
&lt;td&gt;Can't keep the contract → don't inherit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISP&lt;/td&gt;
&lt;td&gt;Fat interface&lt;/td&gt;
&lt;td&gt;Interface explosion&lt;/td&gt;
&lt;td&gt;One interface per role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIP&lt;/td&gt;
&lt;td&gt;Domain imports infrastructure&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FooServiceImpl&lt;/code&gt; everywhere&lt;/td&gt;
&lt;td&gt;Abstract at true frontiers only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The LSP row reads differently on purpose: a constraint isn't under-applied, it's &lt;em&gt;broken&lt;/em&gt;, and the &lt;code&gt;is&lt;/code&gt;-check patches are the symptom you see in callers.&lt;/p&gt;

&lt;p&gt;Cohesion and coupling aren't SOLID's children; they're its grandparents. Stevens, Myers, and Constantine named the pair in "Structured Design" (&lt;em&gt;IBM Systems Journal&lt;/em&gt;, 1974); Parnas nailed the underlying idea as &lt;em&gt;information hiding&lt;/em&gt; in 1972; the acronym arrived three decades later. The letters are the most successful marketing campaign those two ideas ever had, though LSP also carries a correctness rule that neither force gives you on its own. Useful as mnemonics, dangerous as a checklist: a checklist tells you to add an interface, and the forces tell you whether the interface bought you anything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Everything in this article lives in &lt;a href="https://github.com/dionisioC/blog/tree/main/posts/2026-08-solid-cohesion-coupling/code" rel="noopener noreferrer"&gt;the companion repo&lt;/a&gt;: one Gradle project holding the clean slices and the &lt;code&gt;smells&lt;/code&gt; package side by side, with a test pinning each claim (the VIP balance really goes negative, the same cart really charges the PSP once, a gift-card order really can't take a refund, the decorator order really changes the metric) and a &lt;code&gt;main()&lt;/code&gt; you can run.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>solidprinciples</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
