<?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 Cortes Fernandez</title>
    <description>The latest articles on DEV Community by Dionisio Cortes Fernandez (@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 Cortes Fernandez</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>SOLID Without the Acronym: It's Just Cohesion and Coupling</title>
      <dc:creator>Dionisio Cortes Fernandez</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 software design ideas expressed in different ways.&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;Every letter in SOLID is just a &lt;em&gt;named consequence&lt;/em&gt; of one of those two forces. 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;A first cut is all it is — the rest of the article complicates it, and the complications are where the thinking is. Two are worth flagging now. ISP is the contested one: the textbook files it under coupling, and the ISP section explains why both readings are right. LSP is the odd one out: it belongs under coupling because callers couple to the &lt;em&gt;base contract&lt;/em&gt;, never to your subtype, but unlike the other four letters it isn't a dial you can turn too far. It's a correctness constraint — which is what makes it a &lt;em&gt;detector&lt;/em&gt; rather than a design choice.&lt;/p&gt;

&lt;p&gt;Once you see that, you stop memorizing and start deriving — and you learn when &lt;em&gt;not&lt;/em&gt; to apply each one, because every single one of them has a cost. Applied without judgment, SOLID 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. One use case, end to end — &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. Every principle below shows up because the domain &lt;em&gt;forces&lt;/em&gt; it, and principles that share a codebase interact: you'll watch them repair each other.&lt;/p&gt;

&lt;p&gt;For each principle: what it means, where it shows up here, and what it costs when you over-apply it.&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. In our system the broken version is a &lt;code&gt;CheckoutManager&lt;/code&gt; with both &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 and &lt;code&gt;renderReceipt()&lt;/code&gt; answers to Marketing. 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 rule, same source set&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;// shared by both — and that's 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 counting gift-wrap fees toward the loyalty discount. A developer edits &lt;code&gt;rewardedItems()&lt;/code&gt; — the obvious place — and Marketing's receipt silently drops gift wrap from its "points earned on" line. The total moves exactly as Finance asked — every item still charged, a slightly smaller discount — so the diff looks correct. Nobody saw two departments in one edit. That shared private helper is coupling between actors, and a code review can easily miss it, because the class has one name and one obvious topic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same shape in the wild.&lt;/strong&gt; You already apply SRP without naming it: the layered split. The controller changes when the &lt;em&gt;API shape&lt;/em&gt; changes, the service when a &lt;em&gt;business rule&lt;/em&gt; changes, the repository when &lt;em&gt;storage&lt;/em&gt; changes — three reasons, three classes. In this system the same instinct fires once more &lt;em&gt;inside&lt;/em&gt; the service layer, and it's the split &lt;code&gt;CheckoutManager&lt;/code&gt; refused to make: pricing math is &lt;code&gt;PriceCalculator&lt;/code&gt; (Finance's), receipt copy is &lt;code&gt;ReceiptFormatter&lt;/code&gt; (Marketing's), orchestration is &lt;code&gt;CheckoutService&lt;/code&gt; (the product flow). In the repo the gift-wrap edit is a &lt;code&gt;rewardGiftWrap&lt;/code&gt; flag, so &lt;code&gt;SrpTest&lt;/code&gt; holds both versions of the rule side by side and proves the customer is still billed for every item either way — what moves is the discount, and with it Marketing's receipt. The moment "who asks for changes to this?" gets two different 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; SRP has two failure modes. Under-apply it and you get the god class everyone warns about. But over-apply it and you get something just as bad and harder to spot: &lt;strong&gt;shotgun surgery&lt;/strong&gt; — a single logical change now forces edits across ten tiny files, because you scattered things that actually change together. The dial between the two extremes is &lt;strong&gt;cohesion&lt;/strong&gt;: &lt;em&gt;group what changes together.&lt;/em&gt; Splitting by "this method feels different" is how you end up with the ten-file problem; splitting by "these change for different reasons" is SRP.&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; In practice that means: you should be able to add new behavior by adding a new class, not by editing an existing, tested one. The enemy this principle fights is the &lt;code&gt;if/else&lt;/code&gt; that grows a new branch every time a payment method lands:&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;The mechanism that buys you OCP is polymorphism: depend on an abstraction, and add a new &lt;em&gt;implementation&lt;/em&gt; instead of a new &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;. &lt;em&gt;Something&lt;/em&gt; still has to decide which &lt;code&gt;PaymentMethod&lt;/code&gt; to instantiate, and that dispatch point does move when Bizum arrives: somewhere there's one line saying &lt;code&gt;"bizum" is a BizumPayment&lt;/code&gt;, and you will add it. In this system that somewhere is &lt;code&gt;PaymentMethodRegistry&lt;/code&gt;, and you'll see the line 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, where every edit risks the branches already there, and into one registration line in a place with no logic to break. &lt;em&gt;Closed for modification&lt;/em&gt; was never "zero edits anywhere"; it's "no edits where the behavior lives."&lt;/p&gt;

&lt;p&gt;Notice the condition hiding in all of this: the &lt;em&gt;axis of variation&lt;/em&gt; — the one direction along which you expected change to arrive — was &lt;em&gt;known&lt;/em&gt;. OCP pays off exactly where variation is expected, which makes it worth looking at an axis where the opposite holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The inverse case: closed variation.&lt;/strong&gt; You've already seen this type. &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; returns:&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;receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Receipt&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="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="c1"&gt;// add a 4th 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;(Java has the same pair: &lt;code&gt;sealed&lt;/code&gt; types shipped in 17 — JEP 409 — with the exhaustive pattern &lt;code&gt;switch&lt;/code&gt; that completes them finalized in 21, JEP 441.) This is the &lt;strong&gt;deliberate inverse of OCP&lt;/strong&gt;. OCP wants adding a variant to touch nothing; sealed wants adding a variant to &lt;em&gt;break every exhaustive &lt;code&gt;when&lt;/code&gt; at compile time&lt;/em&gt; (one with an &lt;code&gt;else&lt;/code&gt; branch opts out), because for a closed set you own — the states of an order, the outcomes of a payment — a silently unhandled case is the bug. Payment &lt;em&gt;methods&lt;/em&gt; are an open set: anyone may invent one, so OCP and the registry. Payment &lt;em&gt;results&lt;/em&gt; are a closed set: you decide what an outcome can be, so sealed and an exhaustive &lt;code&gt;when&lt;/code&gt;. One domain, both answers. Choosing per axis is the judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; Designing for OCP up front means adding indirection on a guess. The cost is &lt;strong&gt;premature abstraction (YAGNI)&lt;/strong&gt;: an interface with exactly one implementation forever, a plugin system for plugins that never arrive — and every reader now has to chase that interface to find the one place the work happens. The rule that helps: &lt;strong&gt;wait for the second case.&lt;/strong&gt; Add the abstraction when the &lt;em&gt;second&lt;/em&gt; implementation shows up; 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; is not a code-sharing mechanism, it's a published claim.&lt;/strong&gt; "Every promise the parent makes, I keep." LSP is that claim taken seriously.&lt;/p&gt;

&lt;p&gt;The promises are not only the ones written into method signatures. The expensive ones are the properties that hold for an object's entire lifetime — "balance is never negative", "the captured amount never exceeds the authorized amount" — because callers are entitled to assume them without ever checking. That is their whole value, and it's what makes breaking one so costly.&lt;/p&gt;

&lt;p&gt;Broken promises come in two forms, and they're worth seeing side by side because 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 shortly):&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="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;// promise 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;Every caller written against &lt;code&gt;StoreCredit&lt;/code&gt; is entitled to assume &lt;code&gt;balance()&lt;/code&gt; never comes back negative, after &lt;em&gt;any&lt;/em&gt; sequence of calls — reconciliation, the balance the app displays, the liability line Finance reports (unspent store credit is a liability on someone's books) — and none of them re-check, because the promise said they didn't have to. Hand them a &lt;code&gt;VipStoreCredit&lt;/code&gt; and all of them are wrong at once, with no exception, no crash, and not one changed line of &lt;em&gt;their&lt;/em&gt; code. Nothing fails. Everything is quietly incorrect, which is the expensive kind of wrong.&lt;/p&gt;

&lt;p&gt;The guards in the base class matter too. &lt;code&gt;topUp&lt;/code&gt; and &lt;code&gt;redeem&lt;/code&gt; both refuse negative amounts, and &lt;code&gt;Money&lt;/code&gt;'s arithmetic throws on overflow instead of wrapping. Without them, a plain &lt;code&gt;StoreCredit&lt;/code&gt; could go negative on its own, and this example would be blaming inheritance for a bug the parent already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Form 2 — the loud refusal.&lt;/strong&gt; Checkout eventually 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 is still to widen 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 — the opposite failure to Form 1, and the easier one, because at least it announces itself. Throwing isn't the violation on its own — a contract that allows refusal is kept by refusing. This interface 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;; the refund flow — &lt;code&gt;RefundFlow&lt;/code&gt; in the repo — asks for &lt;code&gt;RefundableMethod&lt;/code&gt;; a gift-card refund now fails to &lt;em&gt;compile&lt;/em&gt;, and the support flow (&lt;code&gt;SupportCreditFlow&lt;/code&gt;) issues store credit instead — the class whose promise you just watched a subclass break. And note &lt;em&gt;what&lt;/em&gt; repaired the broken contract: &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; There isn't one, and that's worth saying explicitly: LSP is the exception. SRP, OCP, ISP, DIP are &lt;em&gt;dials&lt;/em&gt; — every one of them can be over-applied. LSP is a correctness constraint: there is no such thing as "too substitutable." Its real job in your toolbox is diagnostic — LSP is the detector for bad inheritance. When a tempting IS-A can't honor the full contract, the answer is never to patch the caller: &lt;code&gt;if (method is GiftCardPayment) skipRefund()&lt;/code&gt; fixes the wrong answer by breaking OCP, and now two principles are broken instead of one. The answer is to 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.&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 don't segregate an interface by chopping it into pieces, you segregate it by &lt;em&gt;role&lt;/em&gt; — one interface per &lt;em&gt;kind of caller&lt;/em&gt;. The question is "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;// no way to move money in scope&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RefundHandler&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;PaymentGateway&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;/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, not aesthetic: the statements screen has no charge or refund method 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 in a payments system, least privilege by default is what an audit asks for; a change to a charging signature no longer touches any read-only client; and the test double for &lt;code&gt;StatementsScreen&lt;/code&gt; stubs one query method instead of a whole PSP (payment service provider). Notice this system has now segregated twice, on two different questions: the &lt;code&gt;RefundableMethod&lt;/code&gt; split cut by &lt;em&gt;the capability an implementation can truly promise&lt;/em&gt;, this one by &lt;em&gt;the role a client actually plays&lt;/em&gt;. They aren't rivals; they compose. A &lt;code&gt;RefundableMethod&lt;/code&gt; &lt;em&gt;decides&lt;/em&gt; a refund is allowed, then calls &lt;code&gt;PaymentGateway.refund&lt;/code&gt; to &lt;em&gt;carry it out&lt;/em&gt; — capability on the domain method, mechanism on the infra port, &lt;code&gt;RefundFlow&lt;/code&gt; and &lt;code&gt;RefundHandler&lt;/code&gt; in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The symptom to look for.&lt;/strong&gt; An adapter full of no-ops — a class whose entire purpose is to supply empty implementations of methods you were forced to declare — is ISP screaming. Wherever you find one, the interface above it was cut by method count instead of by role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off.&lt;/strong&gt; Over-apply it 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. Notice this is exactly SRP's failure pair one level up — under-apply it and you get the fat interface (the god class of contracts), over-apply it and you get fragmentation (shotgun surgery of contracts) — because ISP &lt;em&gt;is&lt;/em&gt; SRP applied to interfaces. Both are the cohesion force, and the dial is the same: 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;, not cohesion — Robert C. Martin's own formulation, "no client should be forced to depend on methods it doesn't use," is a sentence about client coupling. Both framings are correct; they answer different questions. What segregation &lt;em&gt;buys&lt;/em&gt; is decoupling — clients stop depending on methods they never call. 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; The word doing the work is &lt;em&gt;inversion&lt;/em&gt;, and what gets inverted is not "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; 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;. The domain compiles alone, with no Stripe SDK and no AWS on its classpath. That inverted arrow — dependencies pointing &lt;em&gt;inward&lt;/em&gt;, toward policy — is the dependency rule of hexagonal architecture (ports and adapters): "port" is the interface the domain owns, "adapter" is the implementation infra provides. Hexagonal adds more than the arrow — an explicit application boundary, and adapters both for the callers that drive the application and for the systems it calls — but the arrow itself is DIP applied at the module boundary. Here is the same fact drawn as a directory — the repo's actual layout, DIP made physical:&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/            # zero infra imports — a test enforces it
    Money  Cart  Order  Receipt  PaymentResult (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  InMemoryOrderRepository  Meter  KeyStore
    RetryingGateway  MeteredGateway  IdempotentGateway         &amp;lt;- decorators ('by')
  clients/           # also depends on domain — the callers, not the adapters
    StatementsScreen  RefundHandler                            &amp;lt;- ISP's two role-views
  smells/            # the broken examples worth running, 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;One deliberate swap in the runnable repo: so &lt;code&gt;main&lt;/code&gt; runs anywhere with zero credentials, the shipped adapters are an &lt;code&gt;InMemoryOrderRepository&lt;/code&gt; and a no-network &lt;code&gt;StripeClient&lt;/code&gt; stand-in rather than the real Dynamo and Stripe SDKs. The ports can't tell the difference — that a database can become a map in one line of wiring is DIP's whole claim.&lt;/p&gt;

&lt;p&gt;One simplification, too: in the repo, &lt;code&gt;domain&lt;/code&gt; and &lt;code&gt;infrastructure&lt;/code&gt; are packages in a single Gradle project, not separate modules, so the compiler alone wouldn't stop a domain file from importing an adapter. &lt;code&gt;ArchitectureTest&lt;/code&gt; does — it fails if anything in &lt;code&gt;domain/&lt;/code&gt; depends on code outside the domain, Kotlin, or the JDK. 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; with no framework at all is DI in its purest form — and it's DIP too, because the domain owns the interfaces being wired. If your service depends on an interface its own module owns, you have DIP whether or not a container exists.&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; the mock is the &lt;em&gt;evidence&lt;/em&gt;. And if you can't test a class without booting the database, that's DIP 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" — the premature abstraction OCP warned about, moved up a layer. Abstract at &lt;strong&gt;true frontiers&lt;/strong&gt;: 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). Every port in this domain sits on exactly that kind of frontier. 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;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 in this system has to become an object eventually, and there is exactly one place where that's allowed to happen. Here it is — &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="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="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="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 contract&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="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="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// …then Main.kt builds a Cart, runs checkout.checkout(cart), and prints the sealed&lt;/span&gt;
    &lt;span class="c1"&gt;// result — plus the saved order's receipt, ReceiptFormatter getting its turn.&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. The gateway is wrapped three times — &lt;code&gt;IdempotentGateway&lt;/code&gt; so that an order already approved isn't charged again, &lt;code&gt;RetryingGateway&lt;/code&gt; so a failed call is retried, &lt;code&gt;MeteredGateway&lt;/code&gt; so someone can count what happened. Each wrapper holds the &lt;em&gt;port&lt;/em&gt; rather than a concrete class, which is why they stack at all, and their order is a real decision made here in wiring: &lt;code&gt;MeteredGateway(RetryingGateway(…))&lt;/code&gt; counts &lt;em&gt;logical&lt;/em&gt; charges — one, however many retries it takes — while &lt;code&gt;RetryingGateway(MeteredGateway(…))&lt;/code&gt; counts &lt;em&gt;attempts&lt;/em&gt;, every retry included. Attempts aren't PSP calls: with the idempotency layer inside the meter, a replay answered from its cache is counted but never reaches the PSP — to count real PSP calls, wrap the Stripe adapter itself. Neither order is wrong; they're different metrics, and swapping them is a one-line diff in a code review rather than a new class. The registry is OCP's dispatch point, concentrated into the one place with no logic to break — the line the OCP section promised you. Every detail reaches the domain typed as a &lt;em&gt;port&lt;/em&gt; (&lt;code&gt;PaymentGateway&lt;/code&gt;, not &lt;code&gt;StripePaymentGateway&lt;/code&gt;), so ISP's role views and LSP's substitutability are what the rest of the system sees. The domain classes take their details from outside — DIP with no framework in sight, which is DI in its purest form, exactly as promised. And nothing in this function contains business logic, because its single reason to change is "the wiring changed" — SRP, applied to &lt;code&gt;main&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;Know the limits of two of those layers, too. &lt;code&gt;IdempotentGateway&lt;/code&gt; is a local memory of approvals, for calls made one at a time: if the PSP approves a charge but the response is lost, or two calls race past the cache together, it can't help. The guarantee that survives those cases is the same key sent to the PSP, which is why the Stripe adapter forwards it. And the sample retries every result that isn't an approval; production code retries only transient failures like &lt;code&gt;Timeout&lt;/code&gt;, never a hard decline.&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 arrive constantly, 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 role per client&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; — the &lt;code&gt;is&lt;/code&gt;-check patches are the symptom you see in callers, not a sign you used too little LSP.&lt;/p&gt;

&lt;p&gt;Cohesion and coupling are not 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. Genuinely useful as mnemonics, dangerous as a checklist. A checklist tells you to add an interface. 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, 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>
