<?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>Forem: Tiberiu Tofan</title>
    <description>The latest articles on Forem by Tiberiu Tofan (@tibtof).</description>
    <link>https://forem.com/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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F223114%2F30e71a52-5c7c-404f-b3d6-4e9a209719e0.png</url>
      <title>Forem: Tiberiu Tofan</title>
      <link>https://forem.com/tibtof</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tibtof"/>
    <language>en</language>
    <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://forem.com/tibtof/all-you-need-is-lambdas-java-tests-without-a-mocking-framework-1i4</link>
      <guid>https://forem.com/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://forem.com/tibtof/sleep-sort-repeat-testing-kotlin-coroutines-with-virtual-time-25pl</link>
      <guid>https://forem.com/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>
