<?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: Tiberiu Tofan</title>
    <description>The latest articles on DEV Community by Tiberiu Tofan (@tibtof).</description>
    <link>https://dev.to/tibtof</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%2F223114%2F30e71a52-5c7c-404f-b3d6-4e9a209719e0.png</url>
      <title>DEV Community: Tiberiu Tofan</title>
      <link>https://dev.to/tibtof</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tibtof"/>
    <language>en</language>
    <item>
      <title>Your AI Writes Junk, and You Pay for It Twice</title>
      <dc:creator>Tiberiu Tofan</dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:45:21 +0000</pubDate>
      <link>https://dev.to/tibtof/your-ai-writes-junk-and-you-pay-for-it-twice-185c</link>
      <guid>https://dev.to/tibtof/your-ai-writes-junk-and-you-pay-for-it-twice-185c</guid>
      <description>&lt;p&gt;Last week an agent wrote me around ten tests for a function. Two of them were useless. Not wrong, exactly: they re-checked the same branch, asserted things the type system already guaranteed, and added nothing a reviewer would keep. I paid output tokens to generate them, then input tokens to carry them in context on every turn after. The model had no reason to delete them. Neither did the company billing me by the token.&lt;/p&gt;

&lt;p&gt;That's the whole problem in one file. We pay for AI by the unit it produces, not by the value it delivers. Not a scam, not lazy, just &lt;em&gt;misaligned&lt;/em&gt;. And the misalignment leaks much further into your codebase than you'd expect. Let me show you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tax
&lt;/h2&gt;

&lt;p&gt;The marginal cost of a bad token equals the marginal cost of a good one, so a model billed per output token has a built-in pull toward saying more. Imagine paying programmers per keystroke and acting surprised when the functions get long. My two useless tests are a small example of this. Reasoning models make it worse: an internal chain of thought you never read but pay for, so a response showing five hundred visible tokens can quietly burn several thousand.&lt;/p&gt;

&lt;p&gt;The community already feels this. &lt;a href="https://github.com/JuliusBrussee/caveman" rel="noopener noreferrer"&gt;Caveman&lt;/a&gt;, a Claude Code skill with around 70k stars, makes the agent talk like a caveman ("why use many token when few token do trick") and cuts around 65% of output tokens. Think about that for a second: we built tooling to make the agent grunt, because the meter made full sentences a cost problem.&lt;/p&gt;

&lt;p&gt;Worth knowing: "cheaper per token" often means "more expensive per task." &lt;a href="https://arxiv.org/abs/2511.05722" rel="noopener noreferrer"&gt;OckBench&lt;/a&gt; calls this the &lt;em&gt;overthinking tax&lt;/em&gt;. Smaller models burn absurd token counts trying to imitate complex reasoning without getting there, and the benchmark's most extreme result is an open model matching a frontier model at 75% accuracy while spending 26 times the tokens, 42,243 versus 1,603. The headline price is a trap.&lt;/p&gt;

&lt;p&gt;An arXiv paper, &lt;a href="https://arxiv.org/abs/2505.21627" rel="noopener noreferrer"&gt;&lt;em&gt;Is Your LLM Overcharging You?&lt;/em&gt;&lt;/a&gt;, goes further: under per-token pricing, providers have a financial incentive to misreport token counts, and users can't prove, or even know, they're overcharged. The authors' fix is telling: pay per character, a unit the user can count. Even the academics fall back to the most verifiable unit available. Hold that thought.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Ion_Luca_Caragiale" rel="noopener noreferrer"&gt;Caragiale&lt;/a&gt; saw all of this coming in 1884. In his comedy &lt;em&gt;A Lost Letter&lt;/em&gt;, the police chief Pristanda bills the town for forty-four flags and hangs about a dozen. When questioned, he counts them out loud: two at the prefecture, two in the market square, two at the town hall, one at the boys' school, one at the girls' school, one at the hospital, two at the cathedral... and then, two at the prefecture again, padding the tally back up toward the dozen-odd that were really there. When the prefect laughs that he already counted those, Pristanda speeds up, recounts everything with even bolder arithmetic, lands on forty-four exactly, and offers that maybe the wind took down one or two. He even has the provider's defense ready: big family, small salary. You know the modern version: big models, expensive GPUs. The trick only got exposed because Zoe rode through town and counted the flags herself. Remember Zoe.&lt;/p&gt;

&lt;h2&gt;
  
  
  It deforms your architecture
&lt;/h2&gt;

&lt;p&gt;The pricing model doesn't stay on the invoice. It climbs out and draws your architecture diagram. You cap history, cache hard, route cheap paths to small or local models, keep reasoning models off the hot path. In a value-aligned world half of these decisions wouldn't exist.&lt;/p&gt;

&lt;p&gt;I felt this building &lt;a href="https://github.com/httptape/httptape" rel="noopener noreferrer"&gt;httptape&lt;/a&gt;, a Go library for recording, redacting, and replaying HTTP traffic. When the API on the other end is an LLM, every test run is metered. Run the suite a hundred times a day and you've paid a hundred times to assert the same thing. So you record once, replay from a tape, and your tests stop touching the paid boundary. Useful engineering, yes. Also a structure I built because of pricing pressure, not because a user needed it. The economics draw the boxes and you label them afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  "So just charge for outcomes"
&lt;/h2&gt;

&lt;p&gt;This is already happening. &lt;a href="https://www.intercom.com/help/en/articles/7837512-how-fin-pricing-works" rel="noopener noreferrer"&gt;Intercom's Fin&lt;/a&gt; charges about a dollar per resolved conversation, &lt;a href="https://www.zendesk.com/newsroom/articles/zendesk-outcome-based-pricing/" rel="noopener noreferrer"&gt;Zendesk prices per automated resolution&lt;/a&gt;, &lt;a href="https://techcrunch.com/2025/11/21/bret-taylors-sierra-reaches-100m-arr-in-under-two-years/" rel="noopener noreferrer"&gt;Sierra&lt;/a&gt; built a company on it. It shows up where the outcome is cheap to define and cheap to verify. The trouble starts on code. I had three fixes in mind, and watching each one break taught me more than any of them working would have.&lt;/p&gt;

&lt;p&gt;First: charge only if it compiles. This one mostly holds. Compilation is binary, cheap to check, impossible to argue with.&lt;/p&gt;

&lt;p&gt;Second: charge only if the tests pass. A while back I joined a team that inherited a codebase from an outside group. One handover condition was 80% test coverage, a clean contractual outcome. The number was met. Most of the tests had no assertions at all. Nobody had to cheat: coverage measures which lines a test touches, never whether it verifies anything, and the moment it became the graded number it stopped describing what we cared about. Pay an agent against a green suite and the cheapest path to green is what you'd expect: a test that asserts nothing, an assertion quietly weakened, a hard case skipped with a convincing comment.&lt;/p&gt;

&lt;p&gt;And gaming is only the cynical version. When the same agent writes the code and then the tests, the tests are derived from the implementation, bugs included. A wrong boundary condition doesn't get caught, it gets asserted, and now it's green and regression-protected. Colleagues of mine built a skill pushing agents to do TDD, and I keep asking a question I can't answer: does test-first carry any value over to generated code? For humans, TDD's real product was never the tests, it was design pressure on the author. Whether that survives when one model writes both sides of the contract in one go, I haven't seen anyone demonstrate. We're pricing against a signal we haven't even validated. We've all seen the human version anyway, the automatic "LGTM" on a PR nobody read. A green light is cheap to emit and expensive to verify, so it stops meaning anything.&lt;/p&gt;

&lt;p&gt;Third: cut the price when the agent ignores AGENTS.md, which it does constantly. My instructions file says "always use the latest library versions" and "when blocked, stop and ask for guidance, don't try alternative approaches without approval." Copilot once hit a failing test, misread the error output, and started downgrading libraries to make it pass: both rules broken in one loop, chasing the wrong problem. I paid tokens for every step of that loop, and then more tokens to undo it. The disobedience itself is billable. So cutting the price for ignored instructions sounds fair, but you can only subtract for violations you can mechanically detect. "Never use &lt;code&gt;!!&lt;/code&gt;" is a lint rule. "Keep the domain pure, respect the hexagon" is not. The instructions I care about most are the ones I can't auto-verify, which is exactly why they get ignored and exactly why I can't price the violation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verifiability gradient
&lt;/h2&gt;

&lt;p&gt;Lay the three fixes side by side and a pattern shows up underneath them:&lt;/p&gt;

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

&lt;p&gt;Pricing can only attach to the left end. Real value lives on the right. Remember pay-per-character? That's the far left edge, the most countable unit there is. And providers aren't being lazy: their costs really are quantitative, GPU-seconds and tokens, while outcome pricing asks them to pay for the times the model fails. A rational provider only takes that bet where the outcome is cheap to define and hard to fake, which is why your support-ticket vendor can and your model API can't.&lt;/p&gt;

&lt;p&gt;Token pricing won because the countable thing is both the easiest to meter and the easiest to inflate, while the valuable thing sits just past the edge of what we can compute.&lt;/p&gt;

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

&lt;p&gt;The unlock isn't a billing setting. It's whoever builds the layer that can score the right end of that gradient, and that layer has one hard constraint: the thing being measured can't report its own meter. Zoe didn't ask Pristanda how many flags he hung. She counted them from the carriage. &lt;a href="https://vibewarden.dev" rel="noopener noreferrer"&gt;VibeWarden&lt;/a&gt;, the security sidecar I've been building, runs on the same stubborn rule: watch the boundary, don't trust the actor. A verifier for AI-generated code has to work the same way, sitting beside the agent and checking the build, the suite, the architecture, and the rules independently, because anything self-reported is worth as much as an unread LGTM. I feel strongly enough about that last part that I built &lt;a href="https://github.com/tibtof/lgtm-buzzer" rel="noopener noreferrer"&gt;lgtm-buzzer&lt;/a&gt;, a browser extension that quizzes you on the diff before letting you approve a PR. The green light should cost something.&lt;/p&gt;

&lt;p&gt;That's the next thing I want to build: an external referee that measures a little further up the gradient than "it compiled," while being honest that the very top, whether the design is actually good, stays out of reach for any tool, mine included. And one experiment I want to run on the way: same tasks, code-first versus test-first agents, seeded bugs, and a count of which flow's tests actually catch them. That's the next piece.&lt;/p&gt;

&lt;p&gt;Until then, a question for you: would you pay per outcome for generated code? And what signal would you trust to verify it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>programming</category>
      <category>llm</category>
    </item>
    <item>
      <title>All You Need Is Lambdas: Java Tests Without a Mocking Framework</title>
      <dc:creator>Tiberiu Tofan</dc:creator>
      <pubDate>Thu, 07 May 2026 14:34:05 +0000</pubDate>
      <link>https://dev.to/tibtof/all-you-need-is-lambdas-java-tests-without-a-mocking-framework-1i4</link>
      <guid>https://dev.to/tibtof/all-you-need-is-lambdas-java-tests-without-a-mocking-framework-1i4</guid>
      <description>&lt;p&gt;Most Java developers have written something like this. Maybe recently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;should_categorize_new_transactions&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransactionRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockMerchantDirectoryService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MerchantDirectoryService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;categorizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TransactionCategorizer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockMerchantDirectoryService&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createSampleTransactionMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockMerchantDirectoryService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCategoryForMerchant&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mcc&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MerchantInfo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mcc&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"Transportation"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByTransactionId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;any&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getArgument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;categorizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;categorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTransactionId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A unit test for a small piece of business logic, with mocks for the things it talks to. Mockito setup, a few &lt;code&gt;when().thenReturn()&lt;/code&gt; calls to stub behavior, an assertion at the end. It works, and it's been the default in Java testing for almost two decades. Most teams have hundreds of these tests. Some have thousands.&lt;/p&gt;

&lt;p&gt;Here's the same test, written differently:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;should_categorize_new_transactions&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createSampleTransaction&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;categorizedTransactionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransactionId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;categorizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TransactionCategorizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransactionId&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcc&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Transportation"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;categorizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;categorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransactionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Transportation"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expenseCategory&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same assertions, same correctness, no Mockito. Three lambdas where the original had three &lt;code&gt;when().thenReturn()&lt;/code&gt; blocks. No &lt;code&gt;@Mock&lt;/code&gt;, no &lt;code&gt;verify()&lt;/code&gt;, no &lt;code&gt;ArgumentCaptor&lt;/code&gt;. The actual behavior under test isn't buried under setup anymore; it sits in the middle of the test where it belongs.&lt;/p&gt;

&lt;p&gt;The change isn't to the test. It's to the interface the test had to mock. That's what the rest of this article is about: where the Mockito ceremonies came from in the first place, and what kind of interface lets you delete them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The interface that caused this
&lt;/h2&gt;

&lt;p&gt;Here's the repository being mocked in the first test:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Repository&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransactionRepository&lt;/span&gt;
        &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransactionEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransactionEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByTransactionId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransactionEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByClientIdAndExpenseCategory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;expenseCategory&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
            SELECT new fvf4j.demo.domain.CategoryBudget(t.expenseCategory, SUM(t.amount))
            FROM CategorizedTransactionEntity t
            WHERE t.clientId = :clientId
            GROUP BY t.expenseCategory
            """&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategoryBudget&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBudgetsByCategory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clientId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""
            SELECT DISTINCT t.expenseCategory
            FROM CategorizedTransactionEntity t
            WHERE t.clientId = :clientId
            """&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findDistinctExpenseCategoriesByClientId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clientId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Nothing unusual about it. Four query methods plus everything Spring Data adds through &lt;code&gt;JpaRepository&lt;/code&gt;: &lt;code&gt;save&lt;/code&gt;, &lt;code&gt;findById&lt;/code&gt;, &lt;code&gt;findAll&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;existsById&lt;/code&gt;, the paging variants, the example-matching variants, the batch operations. A few dozen methods in total, most of which the application doesn't use, but they come along for the ride because they're part of the contract.&lt;/p&gt;

&lt;p&gt;This is the interface every Java codebase has some version of. It's reasonable. It's idiomatic. Spring's documentation recommends it, the Spring Boot tutorial uses it, every JPA tutorial since 2014 has shown it. Why settle for one responsibility when you can have them all in one interface?&lt;/p&gt;

&lt;p&gt;The problem is what &lt;em&gt;consumers&lt;/em&gt; of this interface end up depending on. &lt;code&gt;TransactionCategorizer&lt;/code&gt; uses exactly two methods from it: &lt;code&gt;findByTransactionId&lt;/code&gt; and &lt;code&gt;save&lt;/code&gt;. To compile, it depends on the entire repository interface. To test, it leans on a framework that can stand in for that whole surface, which is what Mockito is doing in the first test. The two methods the consumer actually uses are the two it stubs. The dozens it doesn't use don't show up in the test setup, but they're still there at compile time. Every method on that interface is part of the consumer's surface area, even when it's silent.&lt;/p&gt;

&lt;p&gt;This is the Interface Segregation Principle in its most concrete form. Consumers shouldn't depend on methods they don't use. When they do, the test setup pays the cost. If only that.&lt;/p&gt;

&lt;p&gt;The interface ties the consumer to JPA. It dictates the contract from the persistence side rather than the domain. And the cost I see most often in production isn't even the testing pain. It's that dependencies like this make it easy for developers to add yet another responsibility to the same service, because the wiring is already there. I've watched a four-method service become a sixteen-method one over a year because nobody had to add a new dependency to add a new feature.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The fat interface invites the fat service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Splitting the boundary
&lt;/h2&gt;

&lt;p&gt;The repository in the previous section answers a question the &lt;em&gt;persistence layer&lt;/em&gt; is asking: "what queries can you run against this entity?" The answer is shaped by JPA's mechanics: methods named after the database access pattern, return types tied to JPA entities, query annotations embedded in the interface.&lt;/p&gt;

&lt;p&gt;A consumer like &lt;code&gt;TransactionCategorizer&lt;/code&gt; is asking different questions: "given a transaction, can someone tell me whether I've already categorized it, and can someone save the result?" Two questions. Two methods. Nothing about JPA, nothing about entities, nothing about the dozens of other things the repository can do.&lt;/p&gt;

&lt;p&gt;When the consumer's question and the persistence layer's answer are mediated by the same interface, the consumer ends up depending on the answer's full surface. The fix is to give the consumer its own interface, one shaped to its question rather than to the implementation that happens to satisfy it.&lt;/p&gt;

&lt;p&gt;Here's one of those interfaces:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FindByTransactionId&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A single method, named after what the consumer wants to do. No &lt;code&gt;JpaRepository&lt;/code&gt; parent, no JPA entities in the signature. The return type is the domain's &lt;code&gt;CategorizedTransaction&lt;/code&gt;, not the persistence layer's &lt;code&gt;CategorizedTransactionEntity&lt;/code&gt;. The interface tells you exactly what it offers and nothing else.&lt;/p&gt;

&lt;p&gt;A few things about the naming worth pausing on. The interface is &lt;code&gt;FindByTransactionId&lt;/code&gt;, not &lt;code&gt;TransactionFinder&lt;/code&gt; or &lt;code&gt;TransactionQueryService&lt;/code&gt;. It's a verb, not a noun. That's deliberate.&lt;/p&gt;

&lt;p&gt;Noun-named interfaces describe what the type &lt;em&gt;is&lt;/em&gt;. Verb-named interfaces describe what the type &lt;em&gt;does&lt;/em&gt;. For functional interfaces, "what it does" is the more honest description, because there's only one thing it does. Calling it a "Service" or "Repository" or "Finder" borrows weight from those patterns to dress up what is essentially a function.&lt;/p&gt;

&lt;p&gt;Verb names also force the question of consumer perspective. &lt;em&gt;FindByTransactionId&lt;/em&gt; names the action the consumer wants to take. &lt;em&gt;TransactionRepository&lt;/em&gt; names the implementation that happens to be available. The first puts the domain in charge of the contract; the second lets the persistence layer dictate it. That's the dependency-direction shift that ports-and-adapters architectures are built around, and it starts at the name.&lt;/p&gt;

&lt;p&gt;The parameter and return types are also worth a glance. &lt;code&gt;TransactionId&lt;/code&gt; instead of &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;ExpenseCategory&lt;/code&gt; instead of &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;CategorizedTransaction&lt;/code&gt; instead of an opaque entity. That's a separate architectural discipline. Making domain types unambiguous at the boundary deserves its own treatment in another post. For now, the segregation argument and the value-object argument pull in the same direction: both are about making the boundary speak the domain's language rather than the implementation's.&lt;/p&gt;

&lt;p&gt;The rest of the segregated interfaces follow the same pattern:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransactionPorts&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
    &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SaveCategorizedTransaction&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt; &lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
    &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FindByTransactionId&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
    &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FindByClientIdAndExpenseCategory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientId&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ExpenseCategory&lt;/span&gt; &lt;span class="n"&gt;expenseCategory&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
    &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FindBudgetsByCategory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategoryBudget&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBudgetsByCategory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientId&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
    &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FindExpenseCategoriesByClient&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findExpenseCategoriesBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientId&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Five interfaces where the original repository had one. Each is a single method. Each is named after the action the consumer takes, not the persistence pattern that satisfies it. None of them inherit from anything Spring-related; the dependency on JPA stays on the implementation side, which is where it belongs.&lt;/p&gt;

&lt;p&gt;Worth noting: &lt;code&gt;CategorizedTransactionPorts&lt;/code&gt; itself isn't a port. It's an outer interface used as a namespace, grouping ports that belong to the same boundary. Java doesn't allow multiple public top-level types in one file, and a class with a private constructor would feel heavier than this needs to be. The outer interface is just a logical container. &lt;code&gt;CategorizedTransactionPorts.FindByTransactionId&lt;/code&gt; reads as "the find-by-transaction-id port that belongs to the categorized-transaction boundary," which is the actual relationship.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;TransactionCategorizer&lt;/code&gt; now declares dependencies only on the ports it actually uses: &lt;code&gt;SaveCategorizedTransaction&lt;/code&gt; and &lt;code&gt;FindByTransactionId&lt;/code&gt;. The other three exist for other consumers. None of them know or care about each other.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the implementations live
&lt;/h2&gt;

&lt;p&gt;A common concern when splitting fat interfaces into many functional interfaces is that the number of implementation classes will explode. It doesn't. A single adapter can implement multiple ports, and usually does:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransactionRepositoryAdapter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt;
        &lt;span class="nc"&gt;SaveCategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;FindByClientIdAndExpenseCategory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;FindByTransactionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;FindExpenseCategoriesByClient&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;FindBudgetsByCategory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransactionJpaRepository&lt;/span&gt; &lt;span class="n"&gt;jpaRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CategorizedTransactionRepositoryAdapter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransactionJpaRepository&lt;/span&gt; &lt;span class="n"&gt;jpaRepository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jpaRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jpaRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt; &lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jpaRepository&lt;/span&gt;
             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransactionEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toDomain&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientId&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ExpenseCategory&lt;/span&gt; &lt;span class="n"&gt;expenseCategory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TransactionId&lt;/span&gt; &lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findExpenseCategoriesBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientId&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategoryBudget&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findBudgetsByCategory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientId&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One adapter, five ports. The class delegates each method to the original Spring Data repository, now renamed &lt;code&gt;CategorizedTransactionJpaRepository&lt;/code&gt; to make its role explicit. The fat interface didn't disappear in the refactor; it got demoted to an implementation detail. The adapter also handles translation between persistence-side and domain-side types. &lt;code&gt;CategorizedTransactionEntity&lt;/code&gt; from the JPA layer becomes &lt;code&gt;CategorizedTransaction&lt;/code&gt; in the domain, and the mapping is the adapter's responsibility, which keeps it out of the consumer's signatures.&lt;/p&gt;

&lt;p&gt;Spring's wiring is straightforward. The adapter is registered as a bean that implements every port interface. Anywhere a domain consumer asks for a &lt;code&gt;FindByTransactionId&lt;/code&gt; or a &lt;code&gt;SaveCategorizedTransaction&lt;/code&gt;, Spring resolves it to this adapter because the adapter implements the port. The consumer doesn't know whether it's getting a Spring bean, a test lambda, or something else, and that's the point.&lt;/p&gt;

&lt;p&gt;The tradeoff is real, even if the class explosion isn't. More small contracts means more names to know, more imports, more places a method might live. Whether that's a net win depends on what your team struggles with more: oversized dependencies that drag too much into every test, or navigating a wider surface of small types. For long-lived domains with multiple consumers, the segregation usually pays off. For a three-controller CRUD app, it probably doesn't.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the second test works
&lt;/h2&gt;

&lt;p&gt;The segregated ports are all single-method interfaces. That means anywhere the consumer expects a port, you can pass a lambda. The consumer doesn't know the difference. As far as &lt;code&gt;TransactionCategorizer&lt;/code&gt; is concerned, it received an implementation of &lt;code&gt;SaveCategorizedTransaction&lt;/code&gt; and is calling it. Whether that implementation is a Spring bean wired up at runtime or a one-line lambda passed by a test, the call site looks the same.&lt;/p&gt;

&lt;p&gt;That's the property the test was using:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;categorizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TransactionCategorizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransactionId&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcc&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Transportation"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three lambdas, three ports. Each lambda satisfies the contract of one port. Their order matches the order of the constructor parameters, which means a reader doesn't even need names to follow what's happening. The first lambda is the save behavior. The second is the find. The third is the merchant category lookup.&lt;/p&gt;

&lt;p&gt;Compare what the Mockito version had to do. The most awkward part was simulating the database's "assign an id on save" behavior:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedTransaction&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getArgument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Capture the argument, cast it, mutate it, return it. Five lines of dance to express what the lambda version expressed in one:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;categorizedTransaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categorizedTransactionId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Take the input, return a copy with an id assigned. No casting, no capture, no framework.&lt;/p&gt;

&lt;p&gt;Worth being precise about what these lambdas are. They're test doubles — specifically, stubs. They return canned values for the test's purposes, nothing more. The article's argument is narrower than "stop using Mockito": it's that when ports are narrow, lambdas are a better tool than a framework for stubbing. Verification is a different question. If a test genuinely needs to assert that a method was called, with which arguments, in which order, then a mocking framework's &lt;code&gt;verify(...)&lt;/code&gt; is the right tool — better than hand-rolling countdown latches and counters. Most tests don't need that. The ones that do still benefit from segregated ports, because the verification is scoped to a single port instead of a fat repository.&lt;/p&gt;

&lt;p&gt;The lambda version uses &lt;code&gt;result.id()&lt;/code&gt; and &lt;code&gt;withId(...)&lt;/code&gt; instead of &lt;code&gt;result.getId()&lt;/code&gt; and &lt;code&gt;setId(...)&lt;/code&gt;. &lt;code&gt;CategorizedTransaction&lt;/code&gt; is no longer a mutable JPA entity; it's an immutable record. That's a real change in the consumer code, and it's a consequence of the boundary fix. When persistence-side types stopped leaking into the consumer's signatures, the consumer was free to use a domain type that didn't need to be a JPA entity.&lt;/p&gt;

&lt;p&gt;The test gets shorter and the consumer code gets cleaner, but neither was the goal. Both came out of giving the boundary the right shape.&lt;/p&gt;
&lt;h2&gt;
  
  
  And in Kotlin
&lt;/h2&gt;

&lt;p&gt;If your codebase is Kotlin, the same pattern gets a small set of upgrades worth knowing about.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fun interface&lt;/code&gt; replaces &lt;code&gt;@FunctionalInterface&lt;/code&gt; and reads as a more honest declaration. Kotlin calls these SAM types, for "single abstract method." The compiler enforces the same constraint either way, but the keyword tells the reader at a glance that this type exists to be passed as a function.&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;fun&lt;/span&gt; &lt;span class="nf"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SaveCategorizedTransaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;operator&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;CategorizedTransaction&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two things changed. The interface is &lt;code&gt;fun interface&lt;/code&gt;, and the method is named &lt;code&gt;invoke&lt;/code&gt; and marked &lt;code&gt;operator&lt;/code&gt;. The first is just nicer syntax. The second is the more interesting choice: any object with an &lt;code&gt;operator fun invoke&lt;/code&gt; can be called like a function. So at the consumer's call site, the port reads as a plain function call:&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;val&lt;/span&gt; &lt;span class="py"&gt;saved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;saveCategorizedTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Not &lt;code&gt;saveCategorizedTransaction.save(transaction)&lt;/code&gt;, not &lt;code&gt;saveCategorizedTransaction.execute(transaction)&lt;/code&gt;. Just the call. Whether the implementation is a class, a lambda, or a method reference, the call site looks identical. The fact that there's an interface in the middle becomes invisible.&lt;/p&gt;

&lt;p&gt;There's also a small organizational difference. The Java version grouped its ports inside an outer &lt;code&gt;CategorizedTransactionPorts&lt;/code&gt; interface as a namespace. That was a workaround for a real Java constraint: one public top-level type per file. To keep five related ports visible together, the Java version needs either a containing type or five separate files sitting next to each other in a package.&lt;/p&gt;

&lt;p&gt;Kotlin doesn't have that constraint. A single file can declare multiple top-level public types, so all five ports live together in one file with no scaffolding:&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;package&lt;/span&gt; &lt;span class="nn"&gt;fvf4k.demo.domain.spi&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;SaveCategorizedTransaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;FindByTransactionId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;FindByClientIdAndExpenseCategory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;FindBudgetsByCategory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In tests this means even less ceremony. Trailing lambda syntax shrinks the construction further:&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;val&lt;/span&gt; &lt;span class="py"&gt;categorizer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TransactionCategorizerService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;findByTransactionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;saveTransaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&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;testId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;resolveExpenseCategory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;ExpenseCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Transportation"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Named parameters, lambdas with implicit &lt;code&gt;it&lt;/code&gt;, no &lt;code&gt;(parameter) -&amp;gt;&lt;/code&gt; boilerplate. The parameter names match the port names: &lt;code&gt;findByTransactionId&lt;/code&gt;, &lt;code&gt;saveTransaction&lt;/code&gt;, &lt;code&gt;resolveExpenseCategory&lt;/code&gt;. The verb-naming discipline from earlier carries through to the construction site. The same test in Kotlin is a few characters shorter than the Java version, but the deeper win is conceptual: at every call site and every test site, the port is indistinguishable from a function, because at that point it is one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The article's hook was Mockito ceremony, and the immediate payoff is real: a test that doesn't need a mocking framework, a consumer that depends only on what it actually uses, a boundary that speaks the domain's language. Those are the practical wins. They're worth having on their own.&lt;/p&gt;

&lt;p&gt;The architectural win sitting underneath is the more durable one. Naming ports after the action a consumer wants to take, instead of after the type that happens to satisfy them, flips the dependency direction. The persistence layer stops dictating the consumer's contract; the consumer dictates its own. Functional interfaces and lambdas are the small mechanism that makes this practical in a language with classes, but the choice that matters is the one made at the name. &lt;em&gt;FindByTransactionId&lt;/em&gt; and &lt;em&gt;TransactionRepository&lt;/em&gt; declare different things about who's in charge of the boundary, and by extension, of the design.&lt;/p&gt;

&lt;p&gt;Frameworks are tools. They shouldn't be the ones writing your architecture.&lt;/p&gt;

&lt;p&gt;None of this is new. Interface Segregation has been the I in SOLID for a long time. What's worth seeing here is the principle at its limit: not just smaller interfaces, but segregation taken seriously enough to shape the boundary itself.&lt;/p&gt;

&lt;p&gt;There's a fuller version of this argument in the project the article's examples come from. It's a refactor of the same domain done twice, in Java and Kotlin, with hexagonal architecture, ArchUnit-enforced layer rules, value objects, and explicit failure handling. The Kotlin version uses Arrow's &lt;code&gt;Raise&lt;/code&gt; for typed errors, which is its own architectural beat and a topic I want to come back to.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/tibtof" rel="noopener noreferrer"&gt;
        tibtof
      &lt;/a&gt; / &lt;a href="https://github.com/tibtof/fun-vs-framework" rel="noopener noreferrer"&gt;
        fun-vs-framework
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Demo project for my talk Own Your Design: Functional Principles vs the Framework
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a href="https://github.com/tibtof/fun-vs-framework/actions/workflows/gradle-build.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/tibtof/fun-vs-framework/actions/workflows/gradle-build.yml/badge.svg" alt="Gradle Build"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Own your design: functional principles vs the framework&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;Demo project to showcase the refactoring of a Spring Boot application from a layered architecture to a hexagonal architecture. The &lt;em&gt;main&lt;/em&gt; branch contains the refactored solution. There are also individual branches for each architectural approach: &lt;a href="https://github.com/tibtof/fun-vs-framework/tree/layered" rel="noopener noreferrer"&gt;layered&lt;/a&gt; and &lt;a href="https://github.com/tibtof/fun-vs-framework/tree/hexagonal" rel="noopener noreferrer"&gt;hexagonal&lt;/a&gt;, with a &lt;a href="https://github.com/tibtof/fun-vs-framework/pull/2" rel="noopener noreferrer"&gt;PR that highlights the changes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To better understand the context I recomend &lt;a href="https://www.youtube.com/watch?v=kqNDeq-DrVM" rel="nofollow noopener noreferrer"&gt;watching my talk&lt;/a&gt; and going over the slides.&lt;/p&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tibtof/fun-vs-framework" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;The talk this code supports, &lt;em&gt;Own Your Design: Functional Principles vs the Framework&lt;/em&gt;, walks through the full refactor with slides and animated examples. The recording and slides are at &lt;a href="https://tibtof.dev/own-your-design/" rel="noopener noreferrer"&gt;https://tibtof.dev/own-your-design/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've worked on a Java or Kotlin codebase where a small number of methods on a fat repository ended up costing you a disproportionate amount of test setup, I'd be curious to hear about it in the comments. Most of us have the same scars. What worked for your team?&lt;/p&gt;

</description>
      <category>java</category>
      <category>kotlin</category>
      <category>spring</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Sleep, Sort, Repeat: Testing Kotlin Coroutines with Virtual Time</title>
      <dc:creator>Tiberiu Tofan</dc:creator>
      <pubDate>Tue, 28 Apr 2026 12:03:40 +0000</pubDate>
      <link>https://dev.to/tibtof/sleep-sort-repeat-testing-kotlin-coroutines-with-virtual-time-25pl</link>
      <guid>https://dev.to/tibtof/sleep-sort-repeat-testing-kotlin-coroutines-with-virtual-time-25pl</guid>
      <description>&lt;p&gt;How long do you think this test takes to run?&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="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`sleep&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="nf"&gt;correctly`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&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;unsorted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextInt&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="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAX_VALUE&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;sorted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unsorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleepSort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unsorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;sorted&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;It sorts 100,000 random integers (up to &lt;code&gt;Int.MAX_VALUE&lt;/code&gt;) using &lt;strong&gt;Sleep Sort&lt;/strong&gt; — an algorithm where each element waits in its own coroutine for a number of &lt;em&gt;seconds&lt;/em&gt; equal to its value, then emits itself. A single element of &lt;code&gt;Int.MAX_VALUE&lt;/code&gt; would take about 68 years to sleep through.&lt;/p&gt;

&lt;p&gt;The test passes in about a second. Not a trick — &lt;code&gt;kotlinx-coroutines-test&lt;/code&gt; is doing something genuinely clever, and it's worth knowing about whether or not you ever sort anything by sleeping. Let me show you.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Sleep Sort?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rosettacode.org/wiki/Sorting_algorithms/Sleep_sort" rel="noopener noreferrer"&gt;Sleep Sort&lt;/a&gt; is the kind of algorithm you find on a wiki at 1am and immediately want to implement. Start a task per element, have each one sleep for a duration proportional to its value, then collect the emissions in order. Smaller values wake up first, larger values wake up later, so the output comes out sorted.&lt;/p&gt;

&lt;p&gt;It's a terrible sorting algorithm — but a fun fit for Kotlin coroutines. Coroutines are cheap (launching 100,000 of them is fine), &lt;code&gt;delay&lt;/code&gt; is suspending rather than blocking (a suspended coroutine occupies no thread), and &lt;code&gt;kotlinx-coroutines-test&lt;/code&gt; can fast-forward through delays. That last part is what makes the test above possible.&lt;/p&gt;
&lt;h2&gt;
  
  
  The implementation
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleepSort&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;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;coroutineScope&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;sorted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;joinAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consumeAsFlow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toList&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;For each element we launch a coroutine that delays for that many seconds, then sends the value into a channel (buffered to &lt;code&gt;size&lt;/code&gt; so sends never suspend). An outer &lt;code&gt;launch&lt;/code&gt; waits for all of them to finish, then closes the channel. Meanwhile &lt;code&gt;consumeAsFlow().toList()&lt;/code&gt; is already collecting — elements arrive in delay-order, which is sorted order.&lt;/p&gt;

&lt;p&gt;A small note on the API: this is restricted to non-negative integers, but not for the reason you'd expect. &lt;code&gt;delay&lt;/code&gt; returns immediately for non-positive durations rather than throwing, so negative inputs all race to the channel at once and produce silently wrong output. A defensive version would start with &lt;code&gt;require(all { it &amp;gt;= 0 })&lt;/code&gt;. Worth knowing about — sometimes a tolerant standard library hides a bug rather than preventing one.&lt;/p&gt;

&lt;p&gt;(The repo has more on the implementation — the role of the outer &lt;code&gt;launch&lt;/code&gt;, and what &lt;code&gt;coroutineScope&lt;/code&gt; does for free. Linked at the end.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Running it for real
&lt;/h2&gt;

&lt;p&gt;First, let's see the algorithm work in real time:&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;suspend&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;unsorted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="py"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;measureTimedValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;unsorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleepSort&lt;/span&gt;&lt;span class="p"&gt;()&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;"It took $duration to sort $sorted"&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;measureTimedValue&lt;/code&gt; runs a block, returns its result, and tells you how long it took — pleasant for this kind of thing.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;It took 5.040657167s to sort [1, 1, 2, 2, 3, 3, 4, 5]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Five seconds for a list of eight integers. The result is sorted, and the runtime is bounded by the largest element, because that's the last coroutine to wake up.&lt;/p&gt;

&lt;p&gt;Now think about that test from the top of the post: 100,000 random integers, each potentially up to &lt;code&gt;Int.MAX_VALUE&lt;/code&gt;. With that many uniformly random values you're virtually guaranteed at least one above two billion, and the largest element drives the runtime — so the test should take decades. It runs in a second. So what's going on?&lt;/p&gt;
&lt;h2&gt;
  
  
  Virtual time
&lt;/h2&gt;

&lt;p&gt;The trick is that &lt;code&gt;runTest&lt;/code&gt;, from &lt;code&gt;kotlinx-coroutines-test&lt;/code&gt;, uses virtual time:&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="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`sleep&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="nf"&gt;correctly`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&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;unsorted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextInt&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="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAX_VALUE&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="p"&gt;(&lt;/span&gt;&lt;span class="py"&gt;defaultSortResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;defaultSortDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;measureTimedValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unsorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sorted&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="p"&gt;(&lt;/span&gt;&lt;span class="py"&gt;sleepSortResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;sleepSortDuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;measureTimedValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unsorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleepSort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultSortResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sleepSortResult&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;"Default sort duration: $defaultSortDuration"&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;"Sleep sort duration: $sleepSortDuration"&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;Same &lt;code&gt;sleepSort&lt;/code&gt;, same 100k integers, same &lt;code&gt;delay(i.seconds)&lt;/code&gt; calls inside — but inside &lt;code&gt;runTest&lt;/code&gt;, those delays don't cost real time:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Default sort duration: 45.769750ms
Sleep sort duration: 1.191051583s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Stdlib's &lt;code&gt;sorted()&lt;/code&gt; still wins by a comfortable margin (it should, it's a real sorting algorithm), but the interesting number is the second one. Sleep sort, an algorithm whose runtime is theoretically bounded by the largest input value in seconds, finishes in 1.2 seconds for 100,000 random integers. That second is real CPU spent shuttling 100,000 coroutines through the scheduler — none of it is simulated delay.&lt;/p&gt;

&lt;p&gt;What &lt;code&gt;runTest&lt;/code&gt; does is swap in a &lt;code&gt;TestCoroutineScheduler&lt;/code&gt; (paired with StandardTestDispatcher) with a &lt;em&gt;virtual clock&lt;/em&gt;. When a coroutine inside the block calls &lt;code&gt;delay(i.seconds)&lt;/code&gt;, the scheduler doesn't actually sleep — it parks the coroutine in a queue keyed by its virtual wake-up time, then runs whatever's ready right now. When nothing's ready, it advances the virtual clock to the next-soonest wake-up and resumes that coroutine. Repeat until everything's done.&lt;/p&gt;

&lt;p&gt;The relative ordering of delays is preserved (a 1-second delay still finishes before a 2-second one), but neither costs you real time. It's a great fit for testing time-dependent coroutine code without sitting around watching the test run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Sleep sort is a joke. &lt;code&gt;runTest&lt;/code&gt; and virtual time are not. The joke was a useful excuse to look at how &lt;code&gt;kotlinx-coroutines-test&lt;/code&gt; lets you write tests for time-dependent coroutine code that finish in milliseconds instead of years.&lt;/p&gt;

&lt;p&gt;The same technique applies anywhere time is the dependency: retry-with-backoff, polling, debounce, scheduled jobs — anywhere a test would otherwise have to wait for something to happen.&lt;/p&gt;

&lt;p&gt;If you want to dig deeper — the implementation details, what happens at the edges (nanoseconds, milliseconds, scaling to millions), and the cases where virtual time will quietly lie to you — there's a "Going deeper" section in the repo's README that picks up where this article leaves off:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/tibtof" rel="noopener noreferrer"&gt;
        tibtof
      &lt;/a&gt; / &lt;a href="https://github.com/tibtof/kotlin-sleep-sort" rel="noopener noreferrer"&gt;
        kotlin-sleep-sort
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sleep Sort implemented in Kotlin with coroutines and virtual time
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;kotlin-sleep-sort&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/tibtof/kotlin-sleep-sort/actions/workflows/build.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/tibtof/kotlin-sleep-sort/actions/workflows/build.yml/badge.svg" alt="Build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Support code for &lt;a href="https://dev.to/tibtof/sleep-sort-repeat-testing-kotlin-coroutines-with-virtual-time-25pl" rel="nofollow"&gt;Sleep, Sort, Repeat&lt;/a&gt; — an article on the sleep sort algorithm and how to test coroutines.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;code&gt;List&amp;lt;Int&amp;gt;.sleepSort()&lt;/code&gt; is a &lt;code&gt;suspend&lt;/code&gt; extension that "sorts" a list of non-negative integers by launching one coroutine per element, delaying each by a duration proportional to the value, and collecting the results in arrival order through a &lt;code&gt;Channel&lt;/code&gt;.&lt;/p&gt;

&lt;div class="highlight highlight-source-kotlin notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;val&lt;/span&gt; sorted &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;listOf&lt;/span&gt;(&lt;span class="pl-c1"&gt;5&lt;/span&gt;, &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-c1"&gt;3&lt;/span&gt;, &lt;span class="pl-c1"&gt;2&lt;/span&gt;, &lt;span class="pl-c1"&gt;4&lt;/span&gt;).sleepSort()
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;//&lt;/span&gt; → [1, 2, 3, 4, 5]&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Each element waits &lt;code&gt;value.seconds&lt;/code&gt; before being emitted, so on a real dispatcher the wall-clock cost grows with the largest element. The interesting part is what coroutines and &lt;code&gt;runTest&lt;/code&gt; let you do about that — see the article.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Build &amp;amp; run&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Requires JDK 21+ (the toolchain will fetch one via &lt;a href="https://github.com/gradle/foojay-toolchains" rel="noopener noreferrer"&gt;foojay&lt;/a&gt; if needed).&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;./gradlew build       &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; compile + test&lt;/span&gt;
./gradlew run         &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; not configured by default — use&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tibtof/kotlin-sleep-sort" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;If you've hit a case where virtual time gave you false confidence — a test passing that failed in production — I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>java</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
