<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://camilyed.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://camilyed.github.io//" rel="alternate" type="text/html" /><updated>2026-06-28T16:37:28+00:00</updated><id>https://camilyed.github.io//feed.xml</id><title type="html">SoftwareJ’s Blog</title><subtitle>A blog about Software Architecture.</subtitle><entry xml:lang="en"><title type="html">@Transactional: when the annotation starts getting in the way</title><link href="https://camilyed.github.io//en/transactional-when-the-annotation-gets-in-the-way/" rel="alternate" type="text/html" title="@Transactional: when the annotation starts getting in the way" /><published>2026-06-28T00:00:00+00:00</published><updated>2026-06-28T00:00:00+00:00</updated><id>https://camilyed.github.io//en/transactional-boundary-post-en</id><content type="html" xml:base="https://camilyed.github.io//en/transactional-when-the-annotation-gets-in-the-way/"><![CDATA[<p><code class="language-plaintext highlighter-rouge">@Transactional</code> is one of those Spring annotations that feels almost built into this popular
framework, so we often add it automatically. I still remember learning the basics of Spring, JPA and
Hibernate back in 2013, and at that time this annotation felt absolutely essential.</p>

<p>You add the annotation to a method, start the application, database writes work, rollback works, tests
are green.</p>

<p>And I really do not want to write an article titled: “<code class="language-plaintext highlighter-rouge">@Transactional</code> is bad, remove it from your
projects”. That would simply be untrue. The problem starts somewhere else: very often the annotation
ends up where it is easiest to paste it, not where the transaction should actually start and end.</p>

<!--more-->

<p>In this article I want to show a few problems I regularly see in code based on <code class="language-plaintext highlighter-rouge">@Transactional</code>, and
an alternative I prefer to use in the application layer: an explicit transaction boundary based on a
lambda.</p>

<p>In this part I focus only on classic, imperative code: JDBC, JPA, Spring Data, <code class="language-plaintext highlighter-rouge">TransactionTemplate</code>.
No WebFlux, no R2DBC, no reactive streams. That deserves a separate article.</p>

<hr />

<h2 id="1-transactional-is-convenient-but-it-hides-the-boundary">1. <code class="language-plaintext highlighter-rouge">@Transactional</code> is convenient, but it hides the boundary</h2>

<p>The simplest example looks harmless:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">AccountService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span><span class="o">;</span>

    <span class="nc">AccountService</span><span class="o">(</span>
            <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">,</span>
            <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">,</span>
            <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span>
    <span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">accountRepository</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">exchangeRateClient</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">historyRepository</span> <span class="o">=</span> <span class="n">historyRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span>
    <span class="nc">AccountSnapshot</span> <span class="nf">exchange</span><span class="o">(</span><span class="nc">ExchangeCurrencyCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">account</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">accountId</span><span class="o">())</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">AccountNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

        <span class="kt">var</span> <span class="n">rate</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">.</span><span class="na">currentUsdRate</span><span class="o">();</span>

        <span class="n">account</span><span class="o">.</span><span class="na">exchangePlnToUsd</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">(),</span> <span class="n">rate</span><span class="o">);</span>

        <span class="n">accountRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">account</span><span class="o">);</span>
        <span class="n">historyRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">AccountHistory</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">account</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">account</span><span class="o">.</span><span class="na">toSnapshot</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>At first glance, everything is OK. One method, one use case, one transaction.</p>

<p>But let’s ask one question: <strong>what should actually be inside the transaction?</strong></p>

<p>Should fetching an exchange rate from an external API keep a database transaction open? Should command
validation run inside a transaction? Should mapping a DTO response run inside a transaction? Should
the whole method be transactional only because we do two database writes at the end?</p>

<p>And this is where the problem starts. <code class="language-plaintext highlighter-rouge">@Transactional</code> on a method makes the whole method
transactional. Not a “part of the method”. Not “only save”. The whole method.</p>

<p>☹️ <strong>Sad little code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Transactional</span>
<span class="nc">AccountSnapshot</span> <span class="nf">exchange</span><span class="o">(</span><span class="nc">ExchangeCurrencyCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

    <span class="kt">var</span> <span class="n">rate</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">.</span><span class="na">currentUsdRate</span><span class="o">();</span> <span class="c1">// external API</span>

    <span class="kt">var</span> <span class="n">account</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">accountId</span><span class="o">())</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">AccountNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

    <span class="n">account</span><span class="o">.</span><span class="na">exchangePlnToUsd</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">(),</span> <span class="n">rate</span><span class="o">);</span>

    <span class="n">accountRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">account</span><span class="o">);</span>
    <span class="n">historyRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">AccountHistory</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">account</span><span class="o">));</span>

    <span class="k">return</span> <span class="n">account</span><span class="o">.</span><span class="na">toSnapshot</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Technically, it works.</p>

<p>The problem is that the transaction starts before validation and also stays open during the external
API call. If the API responds slowly, we hold transactional resources longer than necessary. If there
are locks inside, <code class="language-plaintext highlighter-rouge">SELECT ... FOR UPDATE</code>, higher load, or several parallel requests, things become
less pleasant.</p>

<p>Does it mean every method like this will immediately take production down? No. But this is exactly
the kind of code that “works” for a long time, until one day it starts hurting.</p>

<hr />

<h2 id="2-a-transaction-should-have-the-smallest-readable-scope-possible">2. A transaction should have the smallest readable scope possible</h2>

<p>I prefer when a transaction looks like a conscious decision in code, not a decoration on a class.</p>

<p>Example:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>

    <span class="kt">void</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Runnable</span> <span class="n">operation</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>And a Spring-based implementation:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Component</span>
<span class="kd">class</span> <span class="nc">SpringTransactionBoundary</span> <span class="kd">implements</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">transactionTemplate</span><span class="o">;</span>

    <span class="nc">SpringTransactionBoundary</span><span class="o">(</span><span class="nc">PlatformTransactionManager</span> <span class="n">transactionManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">transactionTemplate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">transactionTemplate</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Runnable</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">transactionTemplate</span><span class="o">.</span><span class="na">executeWithoutResult</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">run</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>From this point, the use case can look like this:</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">AccountService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span><span class="o">;</span>

    <span class="nc">AccountService</span><span class="o">(</span>
            <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">,</span>
            <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">,</span>
            <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">,</span>
            <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span>
    <span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">transaction</span> <span class="o">=</span> <span class="n">transaction</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">accountRepository</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">exchangeRateClient</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">historyRepository</span> <span class="o">=</span> <span class="n">historyRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nc">AccountSnapshot</span> <span class="nf">exchange</span><span class="o">(</span><span class="nc">ExchangeCurrencyCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

        <span class="kt">var</span> <span class="n">rate</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">.</span><span class="na">currentUsdRate</span><span class="o">();</span>

        <span class="k">return</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="kt">var</span> <span class="n">account</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">accountId</span><span class="o">())</span>
                    <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">AccountNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

            <span class="n">account</span><span class="o">.</span><span class="na">exchangePlnToUsd</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">(),</span> <span class="n">rate</span><span class="o">);</span>

            <span class="n">accountRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">account</span><span class="o">);</span>
            <span class="n">historyRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">AccountHistory</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">account</span><span class="o">));</span>

            <span class="k">return</span> <span class="n">account</span><span class="o">.</span><span class="na">toSnapshot</span><span class="o">();</span>
        <span class="o">});</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The difference in code is small, but the difference in intent is big.</p>

<p>Now it is clear that:</p>

<ul>
  <li>validation is outside the transaction,</li>
  <li>the external API call is outside the transaction,</li>
  <li>the transaction contains only reading the current state, changing the aggregate and saving it,</li>
  <li>the boundary is visible exactly where I read the use case.</li>
</ul>

<p>This is not a fight against Spring. Spring still does the work: opens the transaction, commits it,
rolls it back. The difference is that the application layer does not have to guess where exactly the
proxy works and which annotation will be intercepted.</p>

<hr />

<h2 id="3-the-problem-with-private-methods-and-self-invocation">3. The problem with private methods and self-invocation</h2>

<p>This one is a classic.</p>

<p>☹️ <strong>Sad little code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">OrderService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span><span class="o">;</span>

    <span class="nc">OrderService</span><span class="o">(</span><span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">,</span> <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">orderRepository</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">paymentRepository</span> <span class="o">=</span> <span class="n">paymentRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nc">OrderId</span> <span class="nf">create</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
        <span class="k">return</span> <span class="nf">createInsideTransaction</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span>
    <span class="kd">private</span> <span class="nc">OrderId</span> <span class="nf">createInsideTransaction</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">(),</span> <span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
        <span class="n">paymentRepository</span><span class="o">.</span><span class="na">reserve</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">(),</span> <span class="n">order</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This is very easy to miss during code review. We see <code class="language-plaintext highlighter-rouge">@Transactional</code>, and our brain adds: “OK, there
is a transaction”.</p>

<p>But a private method is not a place where a Spring proxy can normally intercept the call. What is
more, even when the method is public, but it is called from the same class through <code class="language-plaintext highlighter-rouge">this.someMethod()</code>,
we run into the self-invocation problem. We do not go through the proxy, so the annotation may not do
what we expect.</p>

<p>With a lambda, the problem disappears, because the transaction does not depend on whether the method
was public, private, called from another bean, or called from the same class.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">OrderService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span><span class="o">;</span>

    <span class="nc">OrderService</span><span class="o">(</span>
            <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">,</span>
            <span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">,</span>
            <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span>
    <span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">transaction</span> <span class="o">=</span> <span class="n">transaction</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">orderRepository</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">paymentRepository</span> <span class="o">=</span> <span class="n">paymentRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nc">OrderId</span> <span class="nf">create</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

        <span class="k">return</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">createInsideTransaction</span><span class="o">(</span><span class="n">command</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">OrderId</span> <span class="nf">createInsideTransaction</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">(),</span> <span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
        <span class="n">paymentRepository</span><span class="o">.</span><span class="na">reserve</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">(),</span> <span class="n">order</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Is the private method a problem now? No. It is just a helper method. The transaction boundary is at
the place where we call <code class="language-plaintext highlighter-rouge">transaction.inTransaction(...)</code>.</p>

<hr />

<h2 id="4-the-annotation-often-mixes-an-architectural-decision-with-a-technical-detail">4. The annotation often mixes an architectural decision with a technical detail</h2>

<p>In practice, <code class="language-plaintext highlighter-rouge">@Transactional</code> often lands on classes like <code class="language-plaintext highlighter-rouge">ApplicationService</code>, <code class="language-plaintext highlighter-rouge">UseCase</code>, <code class="language-plaintext highlighter-rouge">Facade</code>.</p>

<p>In other words, in places where we want to read the business scenario.</p>

<p>And then such a use case starts to look like a Christmas tree:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="nd">@Transactional</span>
<span class="kd">class</span> <span class="nc">RegisterUserService</span> <span class="o">{</span>

    <span class="nd">@Transactional</span><span class="o">(</span><span class="n">readOnly</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="nc">UserView</span> <span class="nf">preview</span><span class="o">(</span><span class="nc">PreviewUserCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span><span class="o">(</span><span class="n">propagation</span> <span class="o">=</span> <span class="nc">Propagation</span><span class="o">.</span><span class="na">REQUIRES_NEW</span><span class="o">)</span>
    <span class="kt">void</span> <span class="nf">auditRegistration</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span><span class="o">(</span><span class="n">timeout</span> <span class="o">=</span> <span class="mi">5</span><span class="o">)</span>
    <span class="nc">UserId</span> <span class="nf">register</span><span class="o">(</span><span class="nc">RegisterUserCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Of course, these settings are sometimes needed. The problem is that they start living as decorations
on methods, not as a readable part of the process.</p>

<p>Why not simply name it like this?</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inReadOnlyTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inNewTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Implementation:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Component</span>
<span class="kd">class</span> <span class="nc">SpringTransactionBoundary</span> <span class="kd">implements</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">required</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">readOnly</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">requiresNew</span><span class="o">;</span>

    <span class="nc">SpringTransactionBoundary</span><span class="o">(</span><span class="nc">PlatformTransactionManager</span> <span class="n">transactionManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">required</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">readOnly</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>
        <span class="k">this</span><span class="o">.</span><span class="na">readOnly</span><span class="o">.</span><span class="na">setReadOnly</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">requiresNew</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>
        <span class="k">this</span><span class="o">.</span><span class="na">requiresNew</span><span class="o">.</span><span class="na">setPropagationBehavior</span><span class="o">(</span><span class="nc">TransactionDefinition</span><span class="o">.</span><span class="na">PROPAGATION_REQUIRES_NEW</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">required</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inReadOnlyTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">readOnly</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inNewTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">requiresNew</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Usage:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">UserView</span> <span class="nf">preview</span><span class="o">(</span><span class="nc">PreviewUserCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inReadOnlyTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findByEmail</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">email</span><span class="o">())</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">UserNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

        <span class="k">return</span> <span class="nc">UserView</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">});</span>
<span class="o">}</span>
</code></pre></div></div>

<p>In the code, I immediately see that this is a read. I do not have to look at the annotation on the
method, on the class, in the interface, in the superclass, or wonder whether the call accidentally
bypasses the proxy.</p>

<hr />

<h2 id="5-long-transactions-are-not-only-a-technical-problem-but-also-a-modelling-problem">5. Long transactions are not only a technical problem, but also a modelling problem</h2>

<p>Very often, long transactions are a symptom that a use case does too much.</p>

<p>☹️ <strong>Sad little code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Transactional</span>
<span class="nc">OrderId</span> <span class="nf">placeOrder</span><span class="o">(</span><span class="nc">PlaceOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">var</span> <span class="n">customer</span> <span class="o">=</span> <span class="n">customerRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">())</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">CustomerNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

    <span class="kt">var</span> <span class="n">pricing</span> <span class="o">=</span> <span class="n">pricingClient</span><span class="o">.</span><span class="na">calculate</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">());</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">place</span><span class="o">(</span><span class="n">customer</span><span class="o">,</span> <span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">(),</span> <span class="n">pricing</span><span class="o">);</span>

    <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>

    <span class="n">invoiceClient</span><span class="o">.</span><span class="na">createInvoice</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
    <span class="n">emailSender</span><span class="o">.</span><span class="na">sendOrderConfirmation</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>

    <span class="n">orderStatusRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">OrderStatus</span><span class="o">.</span><span class="na">confirmed</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">()));</span>

    <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>We have here:</p>

<ul>
  <li>reading the customer,</li>
  <li>a call to an external pricing service,</li>
  <li>saving the order,</li>
  <li>a call to an invoicing system,</li>
  <li>sending an email,</li>
  <li>saving the status.</li>
</ul>

<p>All of this inside one method and one transaction. If the external invoicing system responds after
three seconds, the transaction waits. If the email is not sent, should we roll back the order? Maybe
yes, maybe no. But that should be a business decision, not a side effect of marking the whole method
with a single annotation.</p>

<p>A better direction:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">OrderId</span> <span class="nf">placeOrder</span><span class="o">(</span><span class="nc">PlaceOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

    <span class="kt">var</span> <span class="n">pricing</span> <span class="o">=</span> <span class="n">pricingClient</span><span class="o">.</span><span class="na">calculate</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">());</span>

    <span class="kt">var</span> <span class="n">orderId</span> <span class="o">=</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">customer</span> <span class="o">=</span> <span class="n">customerRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">())</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">CustomerNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

        <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">place</span><span class="o">(</span><span class="n">customer</span><span class="o">,</span> <span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">(),</span> <span class="n">pricing</span><span class="o">);</span>

        <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
        <span class="n">outboxRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">OrderPlacedEvent</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">order</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
    <span class="o">});</span>

    <span class="k">return</span> <span class="n">orderId</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>External side effects can be handled later: through an outbox, an event handler, an asynchronous
process, a scheduler, a queue. You do not always have to bring event-driven architecture to a small
problem, but it is worth building the habit of asking: <strong>does this side effect really have to be in
the same database transaction?</strong></p>

<hr />

<h2 id="6-kotlin-variant">6. Kotlin variant</h2>

<p>In Kotlin, this kind of boundary looks even more natural. In one of my
<a href="https://github.com/CamilYed/currency-exchange-api/tree/main">projects</a>, I used a similar idea with
an <code class="language-plaintext highlighter-rouge">inTransaction { ... }</code> function, because this syntax fits the style of use cases very well.</p>

<p>Minimal version:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="p">{</span>

    <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span>

    <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inReadOnlyTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Spring implementation:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">class</span> <span class="nc">SpringTransactionBoundary</span><span class="p">(</span>
    <span class="n">transactionManager</span><span class="p">:</span> <span class="nc">PlatformTransactionManager</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">TransactionBoundary</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">required</span> <span class="p">=</span> <span class="nc">TransactionTemplate</span><span class="p">(</span><span class="n">transactionManager</span><span class="p">)</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">readOnly</span> <span class="p">=</span> <span class="nc">TransactionTemplate</span><span class="p">(</span><span class="n">transactionManager</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">isReadOnly</span> <span class="p">=</span> <span class="k">true</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">required</span><span class="p">.</span><span class="n">execute</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nf">block</span><span class="p">()</span> <span class="p">}</span> <span class="k">as</span> <span class="nc">T</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inReadOnlyTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">readOnly</span><span class="p">.</span><span class="n">execute</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nf">block</span><span class="p">()</span> <span class="p">}</span> <span class="k">as</span> <span class="nc">T</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Usage in an application service:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">AccountService</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">transaction</span><span class="p">:</span> <span class="nc">TransactionBoundary</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">accountRepository</span><span class="p">:</span> <span class="nc">AccountRepository</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">accountOperationRepository</span><span class="p">:</span> <span class="nc">AccountOperationRepository</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">exchangeRateProvider</span><span class="p">:</span> <span class="nc">ExchangeRateProvider</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>

    <span class="k">fun</span> <span class="nf">exchange</span><span class="p">(</span><span class="n">command</span><span class="p">:</span> <span class="nc">ExchangeCurrencyCommand</span><span class="p">):</span> <span class="nc">AccountSnapshot</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">rate</span> <span class="p">=</span> <span class="n">exchangeRateProvider</span><span class="p">.</span><span class="nf">currentUsdRate</span><span class="p">()</span>

        <span class="k">return</span> <span class="n">transaction</span><span class="p">.</span><span class="nf">inTransaction</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">account</span> <span class="p">=</span> <span class="n">accountRepository</span><span class="p">.</span><span class="nf">findBy</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">accountId</span><span class="p">)</span>
                <span class="o">?:</span> <span class="k">throw</span> <span class="nc">AccountNotFoundException</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">accountId</span><span class="p">)</span>

            <span class="n">account</span><span class="p">.</span><span class="nf">exchangePlnToUsd</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">amount</span><span class="p">,</span> <span class="n">rate</span><span class="p">)</span>

            <span class="n">accountRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">account</span><span class="p">)</span>
            <span class="n">accountOperationRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">account</span><span class="p">.</span><span class="nf">pullEvents</span><span class="p">())</span>

            <span class="n">account</span><span class="p">.</span><span class="nf">toSnapshot</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Is this much more complicated than <code class="language-plaintext highlighter-rouge">@Transactional</code>? No.</p>

<p>But it is more explicit. And for me, that is the main benefit.</p>

<hr />

<h2 id="7-testing-becomes-simpler">7. Testing becomes simpler</h2>

<p>If the use case depends on a small interface:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>then in a unit test I do not need Spring, proxies, or a real transaction manager.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">ImmediateTransactionBoundary</span> <span class="kd">implements</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>And I can test the use case logic normally:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldCreateOrderAndReservePayment</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">transaction</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ImmediateTransactionBoundary</span><span class="o">();</span>
    <span class="kt">var</span> <span class="n">orderRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryOrderRepository</span><span class="o">();</span>
    <span class="kt">var</span> <span class="n">paymentRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryPaymentRepository</span><span class="o">();</span>

    <span class="kt">var</span> <span class="n">service</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OrderService</span><span class="o">(</span><span class="n">transaction</span><span class="o">,</span> <span class="n">orderRepository</span><span class="o">,</span> <span class="n">paymentRepository</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="kt">var</span> <span class="n">orderId</span> <span class="o">=</span> <span class="n">service</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="k">new</span> <span class="nc">CreateOrderCommand</span><span class="o">(</span><span class="s">"customer-1"</span><span class="o">,</span> <span class="nc">BigDecimal</span><span class="o">.</span><span class="na">TEN</span><span class="o">));</span>

    <span class="c1">// then</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">orderRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">)).</span><span class="na">isPresent</span><span class="o">();</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">paymentRepository</span><span class="o">.</span><span class="na">existsFor</span><span class="o">(</span><span class="n">orderId</span><span class="o">)).</span><span class="na">isTrue</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>In an integration test, of course, I still want to verify that the Spring implementation works with
the database. But I do not have to start the whole world for every business test only because there
is a <code class="language-plaintext highlighter-rouge">@Transactional</code> annotation somewhere on a method.</p>

<hr />

<h2 id="8-so-should-we-throw-transactional-away">8. So should we throw <code class="language-plaintext highlighter-rouge">@Transactional</code> away?</h2>

<p>No.</p>

<p>There are places where <code class="language-plaintext highlighter-rouge">@Transactional</code> is good enough:</p>

<ul>
  <li>simple CRUD,</li>
  <li>a small admin application,</li>
  <li>a method that really is one database operation from start to finish,</li>
  <li>infrastructure code where a dependency on Spring does not bother us,</li>
  <li>a quick prototype where an explicit boundary would only add noise.</li>
</ul>

<p>The problem is not that the annotation exists. The problem is that we often use it as the default
answer to every question about data consistency.</p>

<p>And a transaction is a design decision.</p>

<p>Where does a consistent write start? Where does the effect of rollback end? Should the external call
be inside? Should the event be saved together with the aggregate? Should an email roll back the
order? Should audit have its own transaction?</p>

<p>The annotation hides these questions. A lambda-based boundary forces us to see them.</p>

<hr />

<h2 id="summary">Summary</h2>

<p><code class="language-plaintext highlighter-rouge">@Transactional</code> is convenient, but it has a few traps:</p>

<ol>
  <li>It covers the whole method, so it is easy to create a transaction scope that is too wide.</li>
  <li>It works through proxies, so private methods and self-invocation can surprise you.</li>
  <li>It mixes technical configuration with the description of the use case.</li>
  <li>It makes it harder to see what exactly should be rolled back.</li>
  <li>It tempts us to put things inside one transaction that should not be there.</li>
</ol>

<p>The alternative does not have to be complicated. Sometimes a small interface is enough:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="c1">// only what really has to be inside the transaction</span>
<span class="o">});</span>
</code></pre></div></div>

<p>For me, the most important side effect of this approach is that the use case starts telling the truth
about the process. It does not hide boundaries behind an annotation, does not require remembering how
Spring proxies work, and does not pretend that the whole method is always a good transaction scope.</p>

<p>And if the transaction boundary is not visible in code, it is very easy to assume it is where we wish
it was.</p>

<p>Unfortunately, code does not run on wishes.</p>

<hr />

<h2 id="what-next">What next</h2>

<p>This article was about the classic, imperative approach: JDBC/JPA + <code class="language-plaintext highlighter-rouge">TransactionTemplate</code>.</p>

<p>In the reactive world, the topic becomes even more interesting, because Reactor context, <code class="language-plaintext highlighter-rouge">Mono</code>,
<code class="language-plaintext highlighter-rouge">Flux</code>, subscription cancellation and <code class="language-plaintext highlighter-rouge">TransactionalOperator</code> enter the picture. For this reason I
prepared a separate project: <code class="language-plaintext highlighter-rouge">spring-reactive-transaction-boundary</code>.</p>

<p>Repository:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://github.com/CamilYed/spring-reactive-transaction-boundary
</code></pre></div></div>]]></content><author><name></name></author><category term="java" /><category term="kotlin" /><category term="spring-boot" /><category term="transactions" /><category term="clean-architecture" /><summary type="html"><![CDATA[@Transactional is one of those Spring annotations that feels almost built into this popular framework, so we often add it automatically. I still remember learning the basics of Spring, JPA and Hibernate back in 2013, and at that time this annotation felt absolutely essential. You add the annotation to a method, start the application, database writes work, rollback works, tests are green. And I really do not want to write an article titled: “@Transactional is bad, remove it from your projects”. That would simply be untrue. The problem starts somewhere else: very often the annotation ends up where it is easiest to paste it, not where the transaction should actually start and end.]]></summary></entry><entry xml:lang="pl"><title type="html">@Transactional: kiedy adnotacja zaczyna przeszkadzać</title><link href="https://camilyed.github.io//pl/transactional-kiedy-adnotacja-przeszkadza/" rel="alternate" type="text/html" title="@Transactional: kiedy adnotacja zaczyna przeszkadzać" /><published>2026-06-28T00:00:00+00:00</published><updated>2026-06-28T00:00:00+00:00</updated><id>https://camilyed.github.io//pl/o-adnotacji-transactional</id><content type="html" xml:base="https://camilyed.github.io//pl/transactional-kiedy-adnotacja-przeszkadza/"><![CDATA[<p><code class="language-plaintext highlighter-rouge">@Transactional</code> to jedna z tych adnotacji w Springu, które są jakby wbudowane w ten popularny 
framework, że dodajemy ją często z automatu. Pamiętam, w roku 2013 ucząc się podstaw Springa, JPA, Hibernate, że ta
adnotacja była jak na tamte czasy nieodzowna.</p>

<p>Dodajesz adnotację na metodzie, odpalasz aplikację, zapis do bazy działa, rollback działa, testy są zielone.</p>

<p>I naprawdę nie mam zamiaru pisać tekstu pod tytułem: “<code class="language-plaintext highlighter-rouge">@Transactional</code> jest zły, usuńcie go z projektów”. To byłoby
zwyczajnie nieprawdziwe. Problem zaczyna się gdzie indziej: bardzo często adnotacja trafia tam, gdzie najłatwiej ją
wkleić, a nie tam, gdzie faktycznie powinna zaczynać się i kończyć transakcja.</p>

<!--more-->

<p>W tym artykule chciałbym pokazać kilka problemów, które regularnie widzę w kodzie opartym o <code class="language-plaintext highlighter-rouge">@Transactional</code>, oraz
alternatywę, którą wolę stosować w warstwie aplikacyjnej: jawny transaction boundary oparty o lambdę.</p>

<p>W tej części skupiam się wyłącznie na klasycznym, imperatywnym kodzie: JDBC, JPA, Spring Data, <code class="language-plaintext highlighter-rouge">TransactionTemplate</code>.
Bez WebFluxa, bez R2DBC, bez reactive streams. O tym można zrobić osobny tekst.</p>

<hr />

<h2 id="1-transactional-jest-wygodne-ale-ukrywa-granicę">1. <code class="language-plaintext highlighter-rouge">@Transactional</code> jest wygodne, ale ukrywa granicę</h2>

<p>Najprostszy przykład wygląda niewinnie:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">AccountService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span><span class="o">;</span>

    <span class="nc">AccountService</span><span class="o">(</span>
            <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">,</span>
            <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">,</span>
            <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span>
    <span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">accountRepository</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">exchangeRateClient</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">historyRepository</span> <span class="o">=</span> <span class="n">historyRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span>
    <span class="nc">AccountSnapshot</span> <span class="nf">exchange</span><span class="o">(</span><span class="nc">ExchangeCurrencyCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">account</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">accountId</span><span class="o">())</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">AccountNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

        <span class="kt">var</span> <span class="n">rate</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">.</span><span class="na">currentUsdRate</span><span class="o">();</span>

        <span class="n">account</span><span class="o">.</span><span class="na">exchangePlnToUsd</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">(),</span> <span class="n">rate</span><span class="o">);</span>

        <span class="n">accountRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">account</span><span class="o">);</span>
        <span class="n">historyRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">AccountHistory</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">account</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">account</span><span class="o">.</span><span class="na">toSnapshot</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Na pierwszy rzut oka jest OK. Jedna metoda, jeden use case, jedna transakcja.</p>

<p>Ale zadajmy sobie pytanie: <strong>co tak naprawdę powinno być w transakcji?</strong></p>

<p>Czy pobranie kursu waluty z zewnętrznego API powinno trzymać otwartą transakcję bazodanową? Czy walidacja komendy
powinna działać w transakcji? Czy mapowanie odpowiedzi DTO powinno działać w transakcji? Czy cała metoda ma być
transakcyjna tylko dlatego, że na końcu robimy dwa zapisy do bazy?</p>

<p>I tutaj zaczyna się problem. <code class="language-plaintext highlighter-rouge">@Transactional</code> na metodzie robi transakcyjną całą metodę. Nie “fragment metody”.
Nie “tylko save”. Całą metodę.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Transactional</span>
<span class="nc">AccountSnapshot</span> <span class="nf">exchange</span><span class="o">(</span><span class="nc">ExchangeCurrencyCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

    <span class="kt">var</span> <span class="n">rate</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">.</span><span class="na">currentUsdRate</span><span class="o">();</span> <span class="c1">// zewnętrzne API</span>

    <span class="kt">var</span> <span class="n">account</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">accountId</span><span class="o">())</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">AccountNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

    <span class="n">account</span><span class="o">.</span><span class="na">exchangePlnToUsd</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">(),</span> <span class="n">rate</span><span class="o">);</span>

    <span class="n">accountRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">account</span><span class="o">);</span>
    <span class="n">historyRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">AccountHistory</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">account</span><span class="o">));</span>

    <span class="k">return</span> <span class="n">account</span><span class="o">.</span><span class="na">toSnapshot</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Technicznie działa.</p>

<p>Tylko że transakcja zaczyna się przed walidacją i trwa również podczas wywołania zewnętrznego API. Jeśli API odpowiada
wolno, trzymamy zasoby transakcyjne dłużej niż trzeba. Jeśli w środku mamy jeszcze locki, <code class="language-plaintext highlighter-rouge">SELECT ... FOR UPDATE</code>,
większy load albo kilka równoległych requestów, to robi się mniej przyjemnie.</p>

<p>Czy to znaczy, że każda taka metoda od razu położy produkcję? Nie. Ale to jest dokładnie ten typ kodu, który przez
długi czas “działa”, aż pewnego dnia zaczyna boleć.</p>

<hr />

<h2 id="2-transakcja-powinna-mieć-możliwie-mały-i-czytelny-zakres">2. Transakcja powinna mieć możliwie mały i czytelny zakres</h2>

<p>Wolę, kiedy transakcja wygląda jak świadoma decyzja w kodzie, a nie jak dekoracja klasy.</p>

<p>Przykład:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>

    <span class="kt">void</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Runnable</span> <span class="n">operation</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>I implementacja oparta o Springa:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Component</span>
<span class="kd">class</span> <span class="nc">SpringTransactionBoundary</span> <span class="kd">implements</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">transactionTemplate</span><span class="o">;</span>

    <span class="nc">SpringTransactionBoundary</span><span class="o">(</span><span class="nc">PlatformTransactionManager</span> <span class="n">transactionManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">transactionTemplate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">transactionTemplate</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Runnable</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">transactionTemplate</span><span class="o">.</span><span class="na">executeWithoutResult</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">run</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Od tej chwili use case może wyglądać tak:</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">AccountService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span><span class="o">;</span>

    <span class="nc">AccountService</span><span class="o">(</span>
            <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">,</span>
            <span class="nc">AccountRepository</span> <span class="n">accountRepository</span><span class="o">,</span>
            <span class="nc">ExchangeRateClient</span> <span class="n">exchangeRateClient</span><span class="o">,</span>
            <span class="nc">AccountHistoryRepository</span> <span class="n">historyRepository</span>
    <span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">transaction</span> <span class="o">=</span> <span class="n">transaction</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">accountRepository</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">exchangeRateClient</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">historyRepository</span> <span class="o">=</span> <span class="n">historyRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nc">AccountSnapshot</span> <span class="nf">exchange</span><span class="o">(</span><span class="nc">ExchangeCurrencyCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

        <span class="kt">var</span> <span class="n">rate</span> <span class="o">=</span> <span class="n">exchangeRateClient</span><span class="o">.</span><span class="na">currentUsdRate</span><span class="o">();</span>

        <span class="k">return</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="kt">var</span> <span class="n">account</span> <span class="o">=</span> <span class="n">accountRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">accountId</span><span class="o">())</span>
                    <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">AccountNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

            <span class="n">account</span><span class="o">.</span><span class="na">exchangePlnToUsd</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">(),</span> <span class="n">rate</span><span class="o">);</span>

            <span class="n">accountRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">account</span><span class="o">);</span>
            <span class="n">historyRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">AccountHistory</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">account</span><span class="o">));</span>

            <span class="k">return</span> <span class="n">account</span><span class="o">.</span><span class="na">toSnapshot</span><span class="o">();</span>
        <span class="o">});</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Różnica jest mała w kodzie, ale duża w intencji.</p>

<p>Teraz widać, że:</p>

<ul>
  <li>walidacja jest poza transakcją,</li>
  <li>call do zewnętrznego API jest poza transakcją,</li>
  <li>transakcja obejmuje tylko odczyt aktualnego stanu, zmianę agregatu i zapis,</li>
  <li>granica jest widoczna tam, gdzie czytam use case.</li>
</ul>

<p>To nie jest walka ze Springiem. Spring nadal robi robotę: otwiera transakcję, commituje, rollbackuje. Różnica polega na
tym, że warstwa aplikacyjna nie musi zgadywać, gdzie dokładnie działa proxy i która adnotacja zostanie przechwycona.</p>

<hr />

<h2 id="3-problem-prywatnych-metod-i-self-invocation">3. Problem prywatnych metod i self-invocation</h2>

<p>To jest klasyk.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">OrderService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span><span class="o">;</span>

    <span class="nc">OrderService</span><span class="o">(</span><span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">,</span> <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">orderRepository</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">paymentRepository</span> <span class="o">=</span> <span class="n">paymentRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nc">OrderId</span> <span class="nf">create</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
        <span class="k">return</span> <span class="nf">createInsideTransaction</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span>
    <span class="kd">private</span> <span class="nc">OrderId</span> <span class="nf">createInsideTransaction</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">(),</span> <span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
        <span class="n">paymentRepository</span><span class="o">.</span><span class="na">reserve</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">(),</span> <span class="n">order</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Na code review bardzo łatwo to przeoczyć. Widzimy <code class="language-plaintext highlighter-rouge">@Transactional</code>, mózg dopowiada: “OK, transakcja jest”.</p>

<p>Tylko że prywatna metoda nie jest miejscem, w którym Springowe proxy może normalnie przeciąć wywołanie. Co więcej,
nawet gdy metoda jest publiczna, ale wywoływana z tej samej klasy przez <code class="language-plaintext highlighter-rouge">this.someMethod()</code>, wchodzimy w problem
self-invocation. Nie przechodzimy przez proxy, więc adnotacja może nie zrobić tego, czego się spodziewamy.</p>

<p>Z lambdą problem znika, bo transakcja nie zależy od tego, czy metoda była publiczna, prywatna, zawołana z innego beana
czy z tej samej klasy.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">OrderService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span><span class="o">;</span>

    <span class="nc">OrderService</span><span class="o">(</span>
            <span class="nc">TransactionBoundary</span> <span class="n">transaction</span><span class="o">,</span>
            <span class="nc">OrderRepository</span> <span class="n">orderRepository</span><span class="o">,</span>
            <span class="nc">PaymentRepository</span> <span class="n">paymentRepository</span>
    <span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">transaction</span> <span class="o">=</span> <span class="n">transaction</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">orderRepository</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">paymentRepository</span> <span class="o">=</span> <span class="n">paymentRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nc">OrderId</span> <span class="nf">create</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

        <span class="k">return</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">createInsideTransaction</span><span class="o">(</span><span class="n">command</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="nc">OrderId</span> <span class="nf">createInsideTransaction</span><span class="o">(</span><span class="nc">CreateOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">(),</span> <span class="n">command</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
        <span class="n">paymentRepository</span><span class="o">.</span><span class="na">reserve</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">(),</span> <span class="n">order</span><span class="o">.</span><span class="na">amount</span><span class="o">());</span>

        <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Czy prywatna metoda jest teraz problemem? Nie. To zwykła metoda pomocnicza. Granica transakcji jest w miejscu, gdzie
wywołujemy <code class="language-plaintext highlighter-rouge">transaction.inTransaction(...)</code>.</p>

<hr />

<h2 id="4-adnotacja-często-miesza-decyzję-architektoniczną-z-detalem-technicznym">4. Adnotacja często miesza decyzję architektoniczną z detalem technicznym</h2>

<p><code class="language-plaintext highlighter-rouge">@Transactional</code> w praktyce często ląduje na klasach typu <code class="language-plaintext highlighter-rouge">ApplicationService</code>, <code class="language-plaintext highlighter-rouge">UseCase</code>, <code class="language-plaintext highlighter-rouge">Facade</code>.</p>

<p>Czyli w miejscu, gdzie chcemy czytać scenariusz biznesowy.</p>

<p>A potem taki use case zaczyna wyglądać jak choinka:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Service</span>
<span class="nd">@Transactional</span>
<span class="kd">class</span> <span class="nc">RegisterUserService</span> <span class="o">{</span>

    <span class="nd">@Transactional</span><span class="o">(</span><span class="n">readOnly</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
    <span class="nc">UserView</span> <span class="nf">preview</span><span class="o">(</span><span class="nc">PreviewUserCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span><span class="o">(</span><span class="n">propagation</span> <span class="o">=</span> <span class="nc">Propagation</span><span class="o">.</span><span class="na">REQUIRES_NEW</span><span class="o">)</span>
    <span class="kt">void</span> <span class="nf">auditRegistration</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>

    <span class="nd">@Transactional</span><span class="o">(</span><span class="n">timeout</span> <span class="o">=</span> <span class="mi">5</span><span class="o">)</span>
    <span class="nc">UserId</span> <span class="nf">register</span><span class="o">(</span><span class="nc">RegisterUserCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// ...</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Oczywiście, te ustawienia są czasem potrzebne. Problem polega na tym, że zaczynają żyć jako dekoracje metod, a nie jako
czytelny fragment procesu.</p>

<p>Czemu po prostu nie nazwać tego w taki sposób?:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inReadOnlyTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inNewTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Implementacja:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Component</span>
<span class="kd">class</span> <span class="nc">SpringTransactionBoundary</span> <span class="kd">implements</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">required</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">readOnly</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">TransactionTemplate</span> <span class="n">requiresNew</span><span class="o">;</span>

    <span class="nc">SpringTransactionBoundary</span><span class="o">(</span><span class="nc">PlatformTransactionManager</span> <span class="n">transactionManager</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">required</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">readOnly</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>
        <span class="k">this</span><span class="o">.</span><span class="na">readOnly</span><span class="o">.</span><span class="na">setReadOnly</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">requiresNew</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">TransactionTemplate</span><span class="o">(</span><span class="n">transactionManager</span><span class="o">);</span>
        <span class="k">this</span><span class="o">.</span><span class="na">requiresNew</span><span class="o">.</span><span class="na">setPropagationBehavior</span><span class="o">(</span><span class="nc">TransactionDefinition</span><span class="o">.</span><span class="na">PROPAGATION_REQUIRES_NEW</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">required</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inReadOnlyTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">readOnly</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inNewTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">requiresNew</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">status</span> <span class="o">-&gt;</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Użycie:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">UserView</span> <span class="nf">preview</span><span class="o">(</span><span class="nc">PreviewUserCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inReadOnlyTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findByEmail</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">email</span><span class="o">())</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">UserNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

        <span class="k">return</span> <span class="nc">UserView</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">});</span>
<span class="o">}</span>
</code></pre></div></div>

<p>W kodzie od razu widać, że to jest odczyt. Nie muszę patrzeć na adnotację nad metodą, nad klasą, w interfejsie, w
superklasie, albo zastanawiać się, czy przypadkiem wywołanie nie omija proxy.</p>

<hr />

<h2 id="5-długie-transakcje-to-nie-tylko-problem-techniczny-ale-też-modelowania">5. Długie transakcje to nie tylko problem techniczny, ale też modelowania</h2>

<p>Bardzo często długie transakcje są objawem tego, że use case robi za dużo.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Transactional</span>
<span class="nc">OrderId</span> <span class="nf">placeOrder</span><span class="o">(</span><span class="nc">PlaceOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">var</span> <span class="n">customer</span> <span class="o">=</span> <span class="n">customerRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">())</span>
            <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">CustomerNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

    <span class="kt">var</span> <span class="n">pricing</span> <span class="o">=</span> <span class="n">pricingClient</span><span class="o">.</span><span class="na">calculate</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">());</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">place</span><span class="o">(</span><span class="n">customer</span><span class="o">,</span> <span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">(),</span> <span class="n">pricing</span><span class="o">);</span>

    <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>

    <span class="n">invoiceClient</span><span class="o">.</span><span class="na">createInvoice</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
    <span class="n">emailSender</span><span class="o">.</span><span class="na">sendOrderConfirmation</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>

    <span class="n">orderStatusRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">OrderStatus</span><span class="o">.</span><span class="na">confirmed</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">()));</span>

    <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Mamy tutaj:</p>

<ul>
  <li>odczyt klienta,</li>
  <li>call do zewnętrznego pricingu,</li>
  <li>zapis zamówienia,</li>
  <li>call do systemu faktur,</li>
  <li>wysyłkę maila,</li>
  <li>zapis statusu.</li>
</ul>

<p>To wszystko w jednej metodzie i jednej transakcji. Jeśli zewnętrzny system faktur odpowie po 3 sekundach, transakcja
czeka. Jeśli mail nie wyjdzie, rollbackujemy zapis zamówienia? Może tak, może nie. Ale to powinna być decyzja
biznesowa, a nie efekt tego, że cała metoda była oznaczona jedną adnotacją.</p>

<p>Lepszy kierunek:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">OrderId</span> <span class="nf">placeOrder</span><span class="o">(</span><span class="nc">PlaceOrderCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">validate</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>

    <span class="kt">var</span> <span class="n">pricing</span> <span class="o">=</span> <span class="n">pricingClient</span><span class="o">.</span><span class="na">calculate</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">());</span>

    <span class="kt">var</span> <span class="n">orderId</span> <span class="o">=</span> <span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">customer</span> <span class="o">=</span> <span class="n">customerRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">customerId</span><span class="o">())</span>
                <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(</span><span class="nl">CustomerNotFoundException:</span><span class="o">:</span><span class="k">new</span><span class="o">);</span>

        <span class="kt">var</span> <span class="n">order</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">.</span><span class="na">place</span><span class="o">(</span><span class="n">customer</span><span class="o">,</span> <span class="n">command</span><span class="o">.</span><span class="na">items</span><span class="o">(),</span> <span class="n">pricing</span><span class="o">);</span>

        <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">order</span><span class="o">);</span>
        <span class="n">outboxRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">OrderPlacedEvent</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">order</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">order</span><span class="o">.</span><span class="na">id</span><span class="o">();</span>
    <span class="o">});</span>

    <span class="k">return</span> <span class="n">orderId</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Zewnętrzne efekty uboczne można obsłużyć później: przez outbox, event handler, proces asynchroniczny, scheduler,
kolejkę. Nie zawsze trzeba robić event-driven architecture z armaty do muchy, ale warto mieć odruch pytania: <strong>czy ten
efekt uboczny naprawdę musi być w tej samej transakcji bazodanowej?</strong></p>

<hr />

<h2 id="6-kotlinowy-wariant">6. Kotlinowy wariant</h2>

<p>W Kotlinie taki boundary wygląda jeszcze naturalniej. W jednym z moich <a href="https://github.com/CamilYed/currency-exchange-api/tree/main">projektów</a> robiłem podobną rzecz właśnie przez
funkcję <code class="language-plaintext highlighter-rouge">inTransaction { ... }</code>, bo taki zapis bardzo dobrze pasuje do stylu use case’ów.</p>

<p>Minimalna wersja:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="p">{</span>

    <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span>

    <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inReadOnlyTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Implementacja Springowa:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">class</span> <span class="nc">SpringTransactionBoundary</span><span class="p">(</span>
    <span class="n">transactionManager</span><span class="p">:</span> <span class="nc">PlatformTransactionManager</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">TransactionBoundary</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">required</span> <span class="p">=</span> <span class="nc">TransactionTemplate</span><span class="p">(</span><span class="n">transactionManager</span><span class="p">)</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">readOnly</span> <span class="p">=</span> <span class="nc">TransactionTemplate</span><span class="p">(</span><span class="n">transactionManager</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span>
        <span class="n">isReadOnly</span> <span class="p">=</span> <span class="k">true</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">required</span><span class="p">.</span><span class="n">execute</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nf">block</span><span class="p">()</span> <span class="p">}</span> <span class="k">as</span> <span class="nc">T</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="nf">inReadOnlyTransaction</span><span class="p">(</span><span class="n">block</span><span class="p">:</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nc">T</span><span class="p">):</span> <span class="nc">T</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">readOnly</span><span class="p">.</span><span class="n">execute</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span> <span class="nf">block</span><span class="p">()</span> <span class="p">}</span> <span class="k">as</span> <span class="nc">T</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Użycie w serwisie aplikacyjnym:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">class</span> <span class="nc">AccountService</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">transaction</span><span class="p">:</span> <span class="nc">TransactionBoundary</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">accountRepository</span><span class="p">:</span> <span class="nc">AccountRepository</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">accountOperationRepository</span><span class="p">:</span> <span class="nc">AccountOperationRepository</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">exchangeRateProvider</span><span class="p">:</span> <span class="nc">ExchangeRateProvider</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>

    <span class="k">fun</span> <span class="nf">exchange</span><span class="p">(</span><span class="n">command</span><span class="p">:</span> <span class="nc">ExchangeCurrencyCommand</span><span class="p">):</span> <span class="nc">AccountSnapshot</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">rate</span> <span class="p">=</span> <span class="n">exchangeRateProvider</span><span class="p">.</span><span class="nf">currentUsdRate</span><span class="p">()</span>

        <span class="k">return</span> <span class="n">transaction</span><span class="p">.</span><span class="nf">inTransaction</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">account</span> <span class="p">=</span> <span class="n">accountRepository</span><span class="p">.</span><span class="nf">findBy</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">accountId</span><span class="p">)</span>
                <span class="o">?:</span> <span class="k">throw</span> <span class="nc">AccountNotFoundException</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">accountId</span><span class="p">)</span>

            <span class="n">account</span><span class="p">.</span><span class="nf">exchangePlnToUsd</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">amount</span><span class="p">,</span> <span class="n">rate</span><span class="p">)</span>

            <span class="n">accountRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">account</span><span class="p">)</span>
            <span class="n">accountOperationRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">account</span><span class="p">.</span><span class="nf">pullEvents</span><span class="p">())</span>

            <span class="n">account</span><span class="p">.</span><span class="nf">toSnapshot</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Czy to jest dużo bardziej skomplikowane niż <code class="language-plaintext highlighter-rouge">@Transactional</code>? Nie.</p>

<p>Ale jest bardziej jawne. I dla mnie to jest główny zysk.</p>

<hr />

<h2 id="7-testowanie-staje-się-prostsze">7. Testowanie staje się prostsze</h2>

<p>Jeśli use case zależy od małego interfejsu:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>to w teście jednostkowym nie potrzebuję Springa, proxy ani prawdziwego transaction managera.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="nc">ImmediateTransactionBoundary</span> <span class="kd">implements</span> <span class="nc">TransactionBoundary</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">inTransaction</span><span class="o">(</span><span class="nc">Supplier</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">operation</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">operation</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>I testuję logikę use case’a normalnie:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldCreateOrderAndReservePayment</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">transaction</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ImmediateTransactionBoundary</span><span class="o">();</span>
    <span class="kt">var</span> <span class="n">orderRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryOrderRepository</span><span class="o">();</span>
    <span class="kt">var</span> <span class="n">paymentRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryPaymentRepository</span><span class="o">();</span>

    <span class="kt">var</span> <span class="n">service</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OrderService</span><span class="o">(</span><span class="n">transaction</span><span class="o">,</span> <span class="n">orderRepository</span><span class="o">,</span> <span class="n">paymentRepository</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="kt">var</span> <span class="n">orderId</span> <span class="o">=</span> <span class="n">service</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="k">new</span> <span class="nc">CreateOrderCommand</span><span class="o">(</span><span class="s">"customer-1"</span><span class="o">,</span> <span class="nc">BigDecimal</span><span class="o">.</span><span class="na">TEN</span><span class="o">));</span>

    <span class="c1">// then</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">orderRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">orderId</span><span class="o">)).</span><span class="na">isPresent</span><span class="o">();</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">paymentRepository</span><span class="o">.</span><span class="na">existsFor</span><span class="o">(</span><span class="n">orderId</span><span class="o">)).</span><span class="na">isTrue</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>W teście integracyjnym oczywiście nadal chcę sprawdzić, czy implementacja Springowa działa z bazą. Ale nie muszę do
każdego testu biznesowego odpalać całego świata tylko dlatego, że gdzieś na metodzie jest <code class="language-plaintext highlighter-rouge">@Transactional</code>.</p>

<hr />

<h2 id="8-czyli-transactional-wyrzucamy-do-kosza">8. Czyli <code class="language-plaintext highlighter-rouge">@Transactional</code> wyrzucamy do kosza?</h2>

<p>Nie.</p>

<p>Są miejsca, gdzie <code class="language-plaintext highlighter-rouge">@Transactional</code> jest wystarczająco dobre:</p>

<ul>
  <li>prosty CRUD,</li>
  <li>mała aplikacja administracyjna,</li>
  <li>metoda, która naprawdę w całości jest jedną operacją bazodanową,</li>
  <li>kod infrastrukturalny, gdzie nie przeszkadza nam zależność od Springa,</li>
  <li>szybki prototyp, gdzie jawna granica byłaby tylko dodatkowym szumem.</li>
</ul>

<p>Problem nie polega na tym, że adnotacja istnieje. Problem polega na tym, że często używamy jej jako domyślnej odpowiedzi
na każde pytanie o spójność danych.</p>

<p>A transakcja jest decyzją projektową.</p>

<p>Gdzie zaczyna się spójny zapis? Gdzie kończy się wpływ rollbacka? Czy zewnętrzny call powinien być w środku? Czy event
ma być zapisany razem z agregatem? Czy mail ma rollbackować zamówienie? Czy audyt ma mieć własną transakcję?</p>

<p>Adnotacja ukrywa te pytania. Boundary przez lambdę zmusza, żeby je zobaczyć.</p>

<hr />

<h2 id="podsumowanie">Podsumowanie</h2>

<p><code class="language-plaintext highlighter-rouge">@Transactional</code> jest wygodne, ale ma kilka pułapek:</p>

<ol>
  <li>Obejmuje całą metodę, więc łatwo stworzyć zbyt szeroki scope transakcji.</li>
  <li>Działa przez proxy, więc prywatne metody i self-invocation potrafią zaskoczyć.</li>
  <li>Miesza konfigurację techniczną z opisem use case’a.</li>
  <li>Utrudnia zobaczenie, co dokładnie ma zostać rollbackowane.</li>
  <li>Kusi, żeby wrzucać do jednej transakcji rzeczy, które nie powinny tam być.</li>
</ol>

<p>Alternatywa nie musi być skomplikowana. Czasem wystarczy mały interfejs:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">transaction</span><span class="o">.</span><span class="na">inTransaction</span><span class="o">(()</span> <span class="o">-&gt;{</span>
        <span class="c1">// tylko to, co naprawdę ma być w transakcji</span>
        <span class="o">});</span>
</code></pre></div></div>

<p>Dla mnie najważniejszy jest efekt uboczny tego podejścia: kod use case’a zaczyna mówić prawdę o procesie. Nie ukrywa
granic za adnotacją, nie wymaga pamiętania o Springowym proxy i nie udaje, że cała metoda zawsze jest dobrym zakresem
transakcji.</p>

<p>A jeśli granicy transakcji nie widać w kodzie, to bardzo łatwo założyć, że jest tam, gdzie chcielibyśmy, żeby była.</p>

<p>Niestety kod nie działa na życzeniach.</p>

<hr />

<h2 id="co-dalej">Co dalej</h2>

<p>Ten tekst dotyczył klasycznego, imperatywnego podejścia: JDBC/JPA + <code class="language-plaintext highlighter-rouge">TransactionTemplate</code>.</p>

<p>W świecie reaktywnym temat robi się jeszcze ciekawszy, bo dochodzi Reactor context, <code class="language-plaintext highlighter-rouge">Mono</code>, <code class="language-plaintext highlighter-rouge">Flux</code>, anulowanie
subskrypcji i <code class="language-plaintext highlighter-rouge">TransactionalOperator</code>. Z tego powodu przygotowałem osobny projekt:
<code class="language-plaintext highlighter-rouge">spring-reactive-transaction-boundary</code>.</p>

<p>Repozytorium jest tutaj:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://github.com/CamilYed/spring-reactive-transaction-boundary
</code></pre></div></div>]]></content><author><name></name></author><category term="java" /><category term="kotlin" /><category term="spring-boot" /><category term="transactions" /><category term="clean-architecture" /><summary type="html"><![CDATA[@Transactional to jedna z tych adnotacji w Springu, które są jakby wbudowane w ten popularny framework, że dodajemy ją często z automatu. Pamiętam, w roku 2013 ucząc się podstaw Springa, JPA, Hibernate, że ta adnotacja była jak na tamte czasy nieodzowna. Dodajesz adnotację na metodzie, odpalasz aplikację, zapis do bazy działa, rollback działa, testy są zielone. I naprawdę nie mam zamiaru pisać tekstu pod tytułem: “@Transactional jest zły, usuńcie go z projektów”. To byłoby zwyczajnie nieprawdziwe. Problem zaczyna się gdzie indziej: bardzo często adnotacja trafia tam, gdzie najłatwiej ją wkleić, a nie tam, gdzie faktycznie powinna zaczynać się i kończyć transakcja.]]></summary></entry><entry xml:lang="en"><title type="html">From Steelworks to Java</title><link href="https://camilyed.github.io//en/from-steelworks-to-java/" rel="alternate" type="text/html" title="From Steelworks to Java" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://camilyed.github.io//en/from-steelworks-to-java</id><content type="html" xml:base="https://camilyed.github.io//en/from-steelworks-to-java/"><![CDATA[<figure style="margin: 24px 0;">
  <img src="/assets/huta-mlociny-archiwum.jpg" alt="Archival panorama of the steelworks around Młociny" style="width: 100%; height: auto; border-radius: 8px;" />
  <figcaption style="font-size: 0.85em; color: #666; text-align: center; margin-top: 8px;">
    Młociny / Bielany — an archival industrial panorama.
  </figcaption>
</figure>

<p>Bielany is a northern district of Warsaw, a little away from the postcard version of the city.<br />
Around Młociny, you can still feel a mix of residential calm, metro-endpoint rhythm, forest edges, and traces of older industrial Warsaw.<br />
One of those traces is the memory of the steelworks — a place that once shaped the character of this part of the city.<br />
I live nearby, so this industrial background is not an abstract historical note for me, but part of the local landscape.<br />
That is why a simple desk with coffee, Java, and a “Zakład Pracy” mug suddenly feels connected to something bigger.</p>

<p>Młociny, Bielany, somewhere near the old steelworks.</p>

<p>A few chimneys, an industrial landscape, and a completely different kind of workplace today: coffee, Java, two monitors, and a laptop.</p>

<p>Steel was produced here once.<br />
Today, it is code.</p>

<!--more-->

<figure style="margin: 28px 0;">
  <img src="/assets/zaklad-pracy-java-huta.png" alt="Java developer desk in an industrial steelworks-inspired style" style="width: 100%; height: auto; border-radius: 8px;" />
  <figcaption style="font-size: 0.85em; color: #666; text-align: center; margin-top: 8px;">
    My modern workplace: coffee, Java, and monitors.
  </figcaption>
</figure>

<p>I do not want to turn this into a grand metaphor, but I like the contrast.</p>

<p>The steelworks in the background.<br />
Java on the screens.<br />
Coffee next to the keyboard.</p>

<p>Different tools. Different noise. A similar rhythm.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">brewCoffee</span><span class="o">();</span>
<span class="n">writeCode</span><span class="o">();</span>
<span class="n">runTests</span><span class="o">();</span>
<span class="n">commit</span><span class="o">();</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="java" /><category term="software-engineering" /><category term="warsaw" /><category term="bielany" /><category term="mlociny" /><category term="steelworks" /><category term="coffee" /><summary type="html"><![CDATA[Młociny / Bielany — an archival industrial panorama. Bielany is a northern district of Warsaw, a little away from the postcard version of the city. Around Młociny, you can still feel a mix of residential calm, metro-endpoint rhythm, forest edges, and traces of older industrial Warsaw. One of those traces is the memory of the steelworks — a place that once shaped the character of this part of the city. I live nearby, so this industrial background is not an abstract historical note for me, but part of the local landscape. That is why a simple desk with coffee, Java, and a “Zakład Pracy” mug suddenly feels connected to something bigger. Młociny, Bielany, somewhere near the old steelworks. A few chimneys, an industrial landscape, and a completely different kind of workplace today: coffee, Java, two monitors, and a laptop. Steel was produced here once. Today, it is code.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://camilyed.github.io//assets/zaklad-pracy-java-huta.png" /><media:content medium="image" url="https://camilyed.github.io//assets/zaklad-pracy-java-huta.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="pl"><title type="html">Od Huty do Javy</title><link href="https://camilyed.github.io//pl/od-huty-do-javy/" rel="alternate" type="text/html" title="Od Huty do Javy" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://camilyed.github.io//pl/od-huty-do-javy</id><content type="html" xml:base="https://camilyed.github.io//pl/od-huty-do-javy/"><![CDATA[<figure style="margin: 24px 0;">
  <img src="/assets/huta-mlociny-archiwum.jpg" alt="Archiwalna panorama Huty w okolicach Młocin" style="width: 100%; height: auto; border-radius: 8px;" />
  <figcaption style="font-size: 0.85em; color: #666; text-align: center; margin-top: 8px;">
    Młociny / Bielany — archiwalna panorama przemysłowa.
  </figcaption>
</figure>

<p>Bielany to północna dzielnica Warszawy, trochę poza pocztówkowym obrazem miasta.<br />
W okolicach Młocin czuć mieszankę spokojnej, mieszkaniowej codzienności, końcowej stacji metra, bliskości lasu i śladów dawnej przemysłowej Warszawy.<br />
Jednym z takich śladów jest pamięć o Hucie — miejscu, które przez lata wpływało na charakter tej części miasta.<br />
Mieszkam niedaleko, więc ten przemysłowy kontekst nie jest dla mnie abstrakcyjną historią, tylko częścią lokalnego krajobrazu.<br />
Dlatego zwykłe biurko z kawą, Javą i kubkiem „Zakład Pracy” nagle zaczyna łączyć się z czymś większym.</p>

<p>Młociny, Bielany, okolice Huty.</p>

<p>Kilka kominów, przemysłowy krajobraz i zupełnie inny rodzaj zakładu pracy dzisiaj: kawa, Java, dwa monitory i laptop.</p>

<p>Dawniej produkcja stali.<br />
Dzisiaj produkcja kodu.</p>

<!--more-->

<figure style="margin: 28px 0;">
  <img src="/assets/zaklad-pracy-java-huta.png" alt="Biurko programisty Java w industrialnym klimacie Huty" style="width: 100%; height: auto; border-radius: 8px;" />
  <figcaption style="font-size: 0.85em; color: #666; text-align: center; margin-top: 8px;">
    Mój współczesny zakład pracy: kawa, Java i monitory.
  </figcaption>
</figure>

<p>Nie chcę robić z tego wielkiej metafory, ale podoba mi się ten kontrast.</p>

<p>Huta w tle.<br />
Java na ekranach.<br />
Kawa obok klawiatury.</p>

<p>Inne narzędzia. Inny hałas. Podobny rytm.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">brewCoffee</span><span class="o">();</span>
<span class="n">writeCode</span><span class="o">();</span>
<span class="n">runTests</span><span class="o">();</span>
<span class="n">commit</span><span class="o">();</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="java" /><category term="software-engineering" /><category term="warszawa" /><category term="bielany" /><category term="mlociny" /><category term="huta" /><category term="kawa" /><summary type="html"><![CDATA[Młociny / Bielany — archiwalna panorama przemysłowa. Bielany to północna dzielnica Warszawy, trochę poza pocztówkowym obrazem miasta. W okolicach Młocin czuć mieszankę spokojnej, mieszkaniowej codzienności, końcowej stacji metra, bliskości lasu i śladów dawnej przemysłowej Warszawy. Jednym z takich śladów jest pamięć o Hucie — miejscu, które przez lata wpływało na charakter tej części miasta. Mieszkam niedaleko, więc ten przemysłowy kontekst nie jest dla mnie abstrakcyjną historią, tylko częścią lokalnego krajobrazu. Dlatego zwykłe biurko z kawą, Javą i kubkiem „Zakład Pracy” nagle zaczyna łączyć się z czymś większym. Młociny, Bielany, okolice Huty. Kilka kominów, przemysłowy krajobraz i zupełnie inny rodzaj zakładu pracy dzisiaj: kawa, Java, dwa monitory i laptop. Dawniej produkcja stali. Dzisiaj produkcja kodu.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://camilyed.github.io//assets/zaklad-pracy-java-huta.png" /><media:content medium="image" url="https://camilyed.github.io//assets/zaklad-pracy-java-huta.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Kotlin context parameters: not DI, but execution context</title><link href="https://camilyed.github.io//en/kotlin-context-parameters-not-di-but-execution-context/" rel="alternate" type="text/html" title="Kotlin context parameters: not DI, but execution context" /><published>2026-06-05T00:00:00+00:00</published><updated>2026-06-05T00:00:00+00:00</updated><id>https://camilyed.github.io//en/kotlin-context-parameters-not-di-but-execution-context</id><content type="html" xml:base="https://camilyed.github.io//en/kotlin-context-parameters-not-di-but-execution-context/"><![CDATA[<p>When I first saw <code class="language-plaintext highlighter-rouge">context(...)</code> next to a function, the obvious question was:</p>

<blockquote>
  <p>did Kotlin get another way to do dependency injection?</p>
</blockquote>

<p>After playing with it for a while, I do not see it that way.</p>

<p><code class="language-plaintext highlighter-rouge">context parameters</code> do not create objects. They do not manage dependency lifecycles. They do not replace Spring, Koin or Dagger.</p>

<p>They describe something simpler:</p>

<blockquote>
  <p>this function can run only when the required context exists at the call site.</p>
</blockquote>

<p>And that condition is checked by the compiler.</p>

<p>That is the important part. Not runtime. Not a magic provider. Not a global singleton. The compiler.</p>

<h2 id="the-simplest-example">The simplest example</h2>

<p>Take a clock.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">ClockProvider</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">now</span><span class="p">():</span> <span class="nc">Instant</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If a function needs the current time, we can pass the clock as a normal parameter:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">confirmedAt</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">):</span> <span class="nc">Instant</span> <span class="p">=</span>
    <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
</code></pre></div></div>

<p>We can also keep it in the constructor, like in classic dependency injection:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">OrderService</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">confirmedAt</span><span class="p">():</span> <span class="nc">Instant</span> <span class="p">=</span>
        <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Or we can say that the clock is part of the execution context of this function:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirmedAt</span><span class="p">():</span> <span class="nc">Instant</span> <span class="p">=</span>
    <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
</code></pre></div></div>

<p>The call site looks like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">clock</span> <span class="p">=</span> <span class="nc">FixedClockProvider</span><span class="p">(</span>
    <span class="nc">Instant</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s">"2026-06-05T10:15:30Z"</span><span class="p">),</span>
<span class="p">)</span>

<span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">confirmedAt</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Without <code class="language-plaintext highlighter-rouge">context(clock)</code>, this function should not compile.</p>

<p>That is the key difference from a service locator. The dependency is still visible in the function signature, but we do not have to pass it as a regular argument every time.</p>

<h2 id="parameter-constructor-or-context">Parameter, constructor or context?</h2>

<p>I would not treat <code class="language-plaintext highlighter-rouge">context parameters</code> as a new default answer for everything.</p>

<p>For me the split looks like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>normal parameters     -&gt; operation input
constructor injection -&gt; stable object dependencies
context parameters    -&gt; execution context
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">orderId</code>, <code class="language-plaintext highlighter-rouge">amount</code>, <code class="language-plaintext highlighter-rouge">email</code>, <code class="language-plaintext highlighter-rouge">newAddress</code> — these are normal parameters.</p>

<p><code class="language-plaintext highlighter-rouge">OrderRepository</code>, <code class="language-plaintext highlighter-rouge">PaymentGateway</code>, <code class="language-plaintext highlighter-rouge">HttpClient</code> — these often belong in the constructor of a use case or service.</p>

<p><code class="language-plaintext highlighter-rouge">ClockProvider</code>, <code class="language-plaintext highlighter-rouge">UserContext</code>, <code class="language-plaintext highlighter-rouge">DomainEvents</code>, <code class="language-plaintext highlighter-rouge">Transaction</code>, <code class="language-plaintext highlighter-rouge">TenantContext</code>, <code class="language-plaintext highlighter-rouge">CorrelationId</code> — these are good candidates for execution context.</p>

<p>Not always, of course. But this boundary feels much healthier than: “great, now everything goes into <code class="language-plaintext highlighter-rouge">context(...)</code>”.</p>

<h2 id="example-repository">Example repository</h2>

<p>For this article I created a small DDD / hexagonal example repository:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/main/kotlin/io/github/camilyed/contextparameters
├── domain
├── application
└── adapter
</code></pre></div></div>

<p>There is no Spring here. On purpose.</p>

<p>I wanted the example to show the language feature and the design decision, not framework integration.</p>

<p>The domain contains one aggregate: <code class="language-plaintext highlighter-rouge">Order</code>.</p>

<p>An order can be a draft or confirmed:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">sealed</span> <span class="kd">interface</span> <span class="nc">OrderState</span> <span class="p">{</span>

    <span class="n">data</span> <span class="kd">object</span> <span class="nc">Draft</span> <span class="p">:</span> <span class="nc">OrderState</span>

    <span class="kd">data class</span> <span class="nc">Confirmed</span><span class="p">(</span>
        <span class="kd">val</span> <span class="py">confirmedAt</span><span class="p">:</span> <span class="nc">Instant</span><span class="p">,</span>
    <span class="p">)</span> <span class="p">:</span> <span class="nc">OrderState</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The aggregate snapshot is simple:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">OrderSnapshot</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">ownerId</span><span class="p">:</span> <span class="nc">UserId</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">state</span><span class="p">:</span> <span class="nc">OrderState</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Now the rule.</p>

<p>An order can be confirmed only when:</p>

<ul>
  <li>it is still a draft,</li>
  <li>the current user is the owner or can confirm orders,</li>
  <li>confirmation stores the current time,</li>
  <li>confirmation publishes a domain event.</li>
</ul>

<p>Inside the aggregate it looks like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Order</span> <span class="k">private</span> <span class="k">constructor</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">ownerId</span><span class="p">:</span> <span class="nc">UserId</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">var</span> <span class="py">state</span><span class="p">:</span> <span class="nc">OrderState</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">require</span><span class="p">(</span><span class="n">state</span> <span class="p">==</span> <span class="nc">OrderState</span><span class="p">.</span><span class="nc">Draft</span><span class="p">)</span> <span class="p">{</span>
            <span class="s">"Only draft order can be confirmed"</span>
        <span class="p">}</span>

        <span class="nf">require</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">userId</span> <span class="p">==</span> <span class="n">ownerId</span> <span class="p">||</span> <span class="n">user</span><span class="p">.</span><span class="n">canConfirmOrders</span><span class="p">)</span> <span class="p">{</span>
            <span class="s">"User cannot confirm this order"</span>
        <span class="p">}</span>

        <span class="kd">val</span> <span class="py">now</span> <span class="p">=</span> <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>

        <span class="n">state</span> <span class="p">=</span> <span class="nc">OrderState</span><span class="p">.</span><span class="nc">Confirmed</span><span class="p">(</span>
            <span class="n">confirmedAt</span> <span class="p">=</span> <span class="n">now</span><span class="p">,</span>
        <span class="p">)</span>

        <span class="n">events</span><span class="p">.</span><span class="nf">publish</span><span class="p">(</span>
            <span class="nc">OrderConfirmed</span><span class="p">(</span>
                <span class="n">orderId</span> <span class="p">=</span> <span class="n">id</span><span class="p">,</span>
                <span class="n">confirmedAt</span> <span class="p">=</span> <span class="n">now</span><span class="p">,</span>
            <span class="p">),</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is still a normal domain method.</p>

<p>But its signature says something important:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span>
</code></pre></div></div>

<p>In other words:</p>

<blockquote>
  <p>I can confirm an order, but only with a clock, domain events and the current user in context.</p>
</blockquote>

<p>I do not want to keep <code class="language-plaintext highlighter-rouge">ClockProvider</code> in the entity constructor. A clock does not describe an order.</p>

<p>I also do not want to hide the current user behind a global <code class="language-plaintext highlighter-rouge">CurrentUserProvider</code>.</p>

<p>This operation simply runs in a specific context.</p>

<h2 id="application-layer">Application layer</h2>

<p>The use case adds a transaction.</p>

<p>This is where the example becomes more interesting, because the responsibilities are visible:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="nn">io.github.camilyed.contextparameters.application</span>

<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.ClockProvider</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.DomainEvents</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.OrderRepository</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.OrderSnapshot</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.Transaction</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.UserContext</span>

<span class="kd">class</span> <span class="nc">ConfirmOrderUseCase</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">orderRepository</span><span class="p">:</span> <span class="nc">OrderRepository</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="nf">context</span><span class="p">(</span>
        <span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span>
        <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span>
        <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">,</span>
        <span class="n">transaction</span><span class="p">:</span> <span class="nc">Transaction</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">fun</span> <span class="nf">confirm</span><span class="p">(</span><span class="n">command</span><span class="p">:</span> <span class="nc">ConfirmOrderCommand</span><span class="p">):</span> <span class="nc">OrderSnapshot</span> <span class="p">=</span>
        <span class="n">transaction</span><span class="p">.</span><span class="nf">within</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">order</span> <span class="p">=</span> <span class="n">orderRepository</span><span class="p">.</span><span class="nf">findById</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">orderId</span><span class="p">)</span>

            <span class="n">order</span><span class="p">.</span><span class="nf">confirm</span><span class="p">()</span>

            <span class="n">orderRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>

            <span class="n">order</span><span class="p">.</span><span class="nf">toSnapshot</span><span class="p">()</span>
        <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">orderRepository</code> is in the constructor because it is a stable dependency of the use case.</p>

<p><code class="language-plaintext highlighter-rouge">command</code> is a normal parameter because it is the operation input.</p>

<p><code class="language-plaintext highlighter-rouge">clock</code>, <code class="language-plaintext highlighter-rouge">events</code>, <code class="language-plaintext highlighter-rouge">user</code> and <code class="language-plaintext highlighter-rouge">transaction</code> are in <code class="language-plaintext highlighter-rouge">context(...)</code> because they describe the environment in which the operation runs.</p>

<p>This split is the most readable one for me.</p>

<h2 id="what-about-adapters">What about adapters?</h2>

<p>The interfaces live in the domain:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">OrderRepository</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">findById</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">):</span> <span class="nc">Order</span>
    <span class="k">fun</span> <span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">:</span> <span class="nc">Order</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The adapter provides an implementation:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">InMemoryOrderRepository</span> <span class="p">:</span> <span class="nc">OrderRepository</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">orders</span> <span class="p">=</span> <span class="n">mutableMapOf</span><span class="p">&lt;</span><span class="nc">OrderId</span><span class="p">,</span> <span class="nc">Order</span><span class="p">&gt;()</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">findById</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">):</span> <span class="nc">Order</span> <span class="p">=</span>
        <span class="n">orders</span><span class="p">[</span><span class="n">id</span><span class="p">]</span>
            <span class="o">?:</span> <span class="k">throw</span> <span class="nc">OrderNotFoundException</span><span class="p">(</span><span class="s">"Order with id ${id.value} not found"</span><span class="p">)</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">:</span> <span class="nc">Order</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">orders</span><span class="p">[</span><span class="n">order</span><span class="p">.</span><span class="n">id</span><span class="p">]</span> <span class="p">=</span> <span class="n">order</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So the dependency direction stays classic.</p>

<p>The domain knows the abstraction.</p>

<p>The adapter knows the implementation.</p>

<p>The use case gets the repository through the constructor and the execution context through <code class="language-plaintext highlighter-rouge">context parameters</code>.</p>

<h2 id="why-not-service-locator">Why not Service Locator?</h2>

<p>Because Service Locator removes dependencies from the signature.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">clock</span> <span class="p">=</span> <span class="nc">ServiceLocator</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">ClockProvider</span><span class="p">&gt;()</span>
    <span class="kd">val</span> <span class="py">events</span> <span class="p">=</span> <span class="nc">ServiceLocator</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">DomainEvents</span><span class="p">&gt;()</span>
    <span class="kd">val</span> <span class="py">user</span> <span class="p">=</span> <span class="nc">ServiceLocator</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">UserContext</span><span class="p">&gt;()</span>

    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At first glance, the method looks cleaner.</p>

<p>But that cleanliness is borrowed.</p>

<p>Looking at <code class="language-plaintext highlighter-rouge">confirm()</code>, we no longer know what the method needs. We have to open the body. And if something is missing from the locator, we often find out at runtime.</p>

<p>With <code class="language-plaintext highlighter-rouge">context parameters</code>, the dependency is still part of the contract:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span>
</code></pre></div></div>

<p>The reader sees the requirements.</p>

<p>The compiler checks them.</p>

<h2 id="test-without-a-container">Test without a container</h2>

<p>In tests I do not need Spring or a DI container.</p>

<p>I have a fake clock:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">TestingClockProvider</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">var</span> <span class="py">currentInstant</span><span class="p">:</span> <span class="nc">Instant</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ClockProvider</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">now</span><span class="p">():</span> <span class="nc">Instant</span> <span class="p">=</span>
        <span class="n">currentInstant</span>

    <span class="k">fun</span> <span class="nf">setNow</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nc">Instant</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">currentInstant</span> <span class="p">=</span> <span class="n">value</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And a Given-When-Then aggregate test:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`should</span> <span class="n">confirm</span> <span class="n">draft</span> <span class="n">order</span> <span class="k">by</span> <span class="nf">owner`</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// given</span>
    <span class="kd">val</span> <span class="py">order</span> <span class="p">=</span>
        <span class="nc">Order</span><span class="p">.</span><span class="nf">fromSnapshot</span><span class="p">(</span>
            <span class="nf">anOrder</span><span class="p">()</span>
                <span class="p">.</span><span class="nf">withId</span><span class="p">(</span><span class="s">"ORD-123"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">ownedBy</span><span class="p">(</span><span class="s">"user-123"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">build</span><span class="p">(),</span>
        <span class="p">)</span>

    <span class="c1">// and</span>
    <span class="nf">currentUserIs</span><span class="p">(</span><span class="s">"user-123"</span><span class="p">)</span>

    <span class="c1">// and</span>
    <span class="nf">currentTimeIs</span><span class="p">(</span><span class="s">"2026-06-05T10:15:30Z"</span><span class="p">)</span>

    <span class="c1">// when</span>
    <span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">,</span> <span class="n">domainEvents</span><span class="p">,</span> <span class="n">currentUser</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">order</span><span class="p">.</span><span class="nf">confirm</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="c1">// then</span>
    <span class="nf">expectThat</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="nf">toSnapshot</span><span class="p">())</span>
        <span class="p">.</span><span class="nf">isConfirmed</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">wasConfirmedAt</span><span class="p">(</span><span class="s">"2026-06-05T10:15:30Z"</span><span class="p">)</span>

    <span class="c1">// and</span>
    <span class="nf">expectThat</span><span class="p">(</span><span class="n">domainEvents</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">hasPublishedOrderConfirmed</span><span class="p">(</span>
            <span class="n">orderId</span> <span class="p">=</span> <span class="s">"ORD-123"</span><span class="p">,</span>
            <span class="n">confirmedAt</span> <span class="p">=</span> <span class="s">"2026-06-05T10:15:30Z"</span><span class="p">,</span>
        <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is still a normal Given-When-Then test.</p>

<p>The test context is explicit:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">,</span> <span class="n">domainEvents</span><span class="p">,</span> <span class="n">currentUser</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">order</span><span class="p">.</span><span class="nf">confirm</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But the domain operation itself does not have a noisy parameter list.</p>

<h2 id="where-would-i-use-it">Where would I use it?</h2>

<p>For things that are execution context:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">Order</span><span class="p">.</span><span class="nf">isExpired</span><span class="p">():</span> <span class="nc">Boolean</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">Document</span><span class="p">.</span><span class="nf">canBeEdited</span><span class="p">():</span> <span class="nc">Boolean</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">tx</span><span class="p">:</span> <span class="nc">Transaction</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">OrderRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">:</span> <span class="nc">Order</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">Order</span><span class="p">.</span><span class="nf">markAsPaid</span><span class="p">()</span>
</code></pre></div></div>

<p>These are not the main input data.</p>

<p>They are also not always stable object dependencies.</p>

<p>They are the environment in which a piece of logic makes sense.</p>

<h2 id="where-would-i-not-use-it">Where would I not use it?</h2>

<p>I would not put half of the application into <code class="language-plaintext highlighter-rouge">context(...)</code>.</p>

<p>☹️ Sad code:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span>
    <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">,</span>
    <span class="n">tenant</span><span class="p">:</span> <span class="nc">TenantContext</span><span class="p">,</span>
    <span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span>
    <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span>
    <span class="n">logger</span><span class="p">:</span> <span class="nc">Logger</span><span class="p">,</span>
    <span class="n">paymentGateway</span><span class="p">:</span> <span class="nc">PaymentGateway</span><span class="p">,</span>
    <span class="n">orderRepository</span><span class="p">:</span> <span class="nc">OrderRepository</span><span class="p">,</span>
    <span class="n">invoiceClient</span><span class="p">:</span> <span class="nc">InvoiceClient</span><span class="p">,</span>
    <span class="n">emailSender</span><span class="p">:</span> <span class="nc">EmailSender</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirmOrder</span><span class="p">(</span><span class="n">orderId</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is not execution context.</p>

<p>This is a DI container written with different syntax.</p>

<p>I also would not put normal input data there:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">orderId</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirmOrder</span><span class="p">()</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">orderId</code> is an operation argument.</p>

<p>Not context.</p>

<p>This is clearer:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">confirmOrder</span><span class="p">(</span><span class="n">orderId</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">)</span>
</code></pre></div></div>

<p>Not everything has to use the new syntax.</p>

<h2 id="watch-out-for-overly-generic-types">Watch out for overly generic types</h2>

<p>Context is resolved by types, so I would be careful with primitives and generic types.</p>

<p>☹️ Sad code:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">loadSomething</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Which string?</p>

<p>User id?</p>

<p>Tenant id?</p>

<p>Correlation id?</p>

<p>A small type is better:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@JvmInline</span>
<span class="n">value</span> <span class="kd">class</span> <span class="nc">UserId</span><span class="p">(</span><span class="kd">val</span> <span class="py">value</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>

<span class="nd">@JvmInline</span>
<span class="n">value</span> <span class="kd">class</span> <span class="nc">TenantId</span><span class="p">(</span><span class="kd">val</span> <span class="py">value</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>

<span class="kd">data class</span> <span class="nc">TenantContext</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">tenantId</span><span class="p">:</span> <span class="nc">TenantId</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Good types matter even more with <code class="language-plaintext highlighter-rouge">context parameters</code>, because the compiler uses them to find the required context.</p>

<h2 id="summary">Summary</h2>

<p><code class="language-plaintext highlighter-rouge">context parameters</code> are not a replacement for DI.</p>

<p>They do not create objects.</p>

<p>They do not manage lifecycles.</p>

<p>They do not replace constructors.</p>

<p>The split that works best for me is:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>constructor injection -&gt; stable object dependencies
normal parameters     -&gt; operation input
context parameters    -&gt; execution context
</code></pre></div></div>

<p>In a small domain fragment, this can clean up the code nicely.</p>

<p>Not because parameters magically disappear.</p>

<p>Because we name their role better.</p>

<p>Example repository: <a href="https://github.com/CamilYed/kotlin-context-parameters-ddd">kotlin-context-parameters-ddd</a></p>

<p>Technical sources:</p>

<ul>
  <li><a href="https://kotlinlang.org/docs/context-parameters.html">Context parameters - Kotlin Documentation</a></li>
  <li><a href="https://kotlinlang.org/docs/whatsnew24.html">What’s new in Kotlin 2.4.0</a></li>
</ul>]]></content><author><name></name></author><category term="kotlin" /><category term="ddd" /><category term="testing" /><category term="software-engineering" /><summary type="html"><![CDATA[When I first saw context(...) next to a function, the obvious question was: did Kotlin get another way to do dependency injection? After playing with it for a while, I do not see it that way. context parameters do not create objects. They do not manage dependency lifecycles. They do not replace Spring, Koin or Dagger. They describe something simpler: this function can run only when the required context exists at the call site. And that condition is checked by the compiler. That is the important part. Not runtime. Not a magic provider. Not a global singleton. The compiler. The simplest example Take a clock. interface ClockProvider { fun now(): Instant } If a function needs the current time, we can pass the clock as a normal parameter: fun confirmedAt(clock: ClockProvider): Instant = clock.now() We can also keep it in the constructor, like in classic dependency injection: class OrderService( private val clock: ClockProvider, ) { fun confirmedAt(): Instant = clock.now() } Or we can say that the clock is part of the execution context of this function: context(clock: ClockProvider) fun confirmedAt(): Instant = clock.now() The call site looks like this: val clock = FixedClockProvider( Instant.parse("2026-06-05T10:15:30Z"), ) context(clock) { confirmedAt() } Without context(clock), this function should not compile. That is the key difference from a service locator. The dependency is still visible in the function signature, but we do not have to pass it as a regular argument every time. Parameter, constructor or context? I would not treat context parameters as a new default answer for everything. For me the split looks like this: normal parameters -&gt; operation input constructor injection -&gt; stable object dependencies context parameters -&gt; execution context orderId, amount, email, newAddress — these are normal parameters. OrderRepository, PaymentGateway, HttpClient — these often belong in the constructor of a use case or service. ClockProvider, UserContext, DomainEvents, Transaction, TenantContext, CorrelationId — these are good candidates for execution context. Not always, of course. But this boundary feels much healthier than: “great, now everything goes into context(...)”. Example repository For this article I created a small DDD / hexagonal example repository: src/main/kotlin/io/github/camilyed/contextparameters ├── domain ├── application └── adapter There is no Spring here. On purpose. I wanted the example to show the language feature and the design decision, not framework integration. The domain contains one aggregate: Order. An order can be a draft or confirmed: sealed interface OrderState { data object Draft : OrderState data class Confirmed( val confirmedAt: Instant, ) : OrderState } The aggregate snapshot is simple: data class OrderSnapshot( val id: OrderId, val ownerId: UserId, val state: OrderState, ) Now the rule. An order can be confirmed only when: it is still a draft, the current user is the owner or can confirm orders, confirmation stores the current time, confirmation publishes a domain event. Inside the aggregate it looks like this: class Order private constructor( val id: OrderId, val ownerId: UserId, private var state: OrderState, ) { context(clock: ClockProvider, events: DomainEvents, user: UserContext) fun confirm() { require(state == OrderState.Draft) { "Only draft order can be confirmed" } require(user.userId == ownerId || user.canConfirmOrders) { "User cannot confirm this order" } val now = clock.now() state = OrderState.Confirmed( confirmedAt = now, ) events.publish( OrderConfirmed( orderId = id, confirmedAt = now, ), ) } } This is still a normal domain method. But its signature says something important: context(clock: ClockProvider, events: DomainEvents, user: UserContext) fun confirm() In other words: I can confirm an order, but only with a clock, domain events and the current user in context. I do not want to keep ClockProvider in the entity constructor. A clock does not describe an order. I also do not want to hide the current user behind a global CurrentUserProvider. This operation simply runs in a specific context. Application layer The use case adds a transaction. This is where the example becomes more interesting, because the responsibilities are visible: package io.github.camilyed.contextparameters.application import io.github.camilyed.contextparameters.domain.ClockProvider import io.github.camilyed.contextparameters.domain.DomainEvents import io.github.camilyed.contextparameters.domain.OrderRepository import io.github.camilyed.contextparameters.domain.OrderSnapshot import io.github.camilyed.contextparameters.domain.Transaction import io.github.camilyed.contextparameters.domain.UserContext class ConfirmOrderUseCase( private val orderRepository: OrderRepository, ) { context( clock: ClockProvider, events: DomainEvents, user: UserContext, transaction: Transaction, ) fun confirm(command: ConfirmOrderCommand): OrderSnapshot = transaction.within { val order = orderRepository.findById(command.orderId) order.confirm() orderRepository.save(order) order.toSnapshot() } } orderRepository is in the constructor because it is a stable dependency of the use case. command is a normal parameter because it is the operation input. clock, events, user and transaction are in context(...) because they describe the environment in which the operation runs. This split is the most readable one for me. What about adapters? The interfaces live in the domain: interface OrderRepository { fun findById(id: OrderId): Order fun save(order: Order) } The adapter provides an implementation: class InMemoryOrderRepository : OrderRepository { private val orders = mutableMapOf&lt;OrderId, Order&gt;() override fun findById(id: OrderId): Order = orders[id] ?: throw OrderNotFoundException("Order with id ${id.value} not found") override fun save(order: Order) { orders[order.id] = order } } So the dependency direction stays classic. The domain knows the abstraction. The adapter knows the implementation. The use case gets the repository through the constructor and the execution context through context parameters. Why not Service Locator? Because Service Locator removes dependencies from the signature. fun confirm() { val clock = ServiceLocator.get&lt;ClockProvider&gt;() val events = ServiceLocator.get&lt;DomainEvents&gt;() val user = ServiceLocator.get&lt;UserContext&gt;() // ... } At first glance, the method looks cleaner. But that cleanliness is borrowed. Looking at confirm(), we no longer know what the method needs. We have to open the body. And if something is missing from the locator, we often find out at runtime. With context parameters, the dependency is still part of the contract: context(clock: ClockProvider, events: DomainEvents, user: UserContext) fun confirm() The reader sees the requirements. The compiler checks them. Test without a container In tests I do not need Spring or a DI container. I have a fake clock: class TestingClockProvider( private var currentInstant: Instant, ) : ClockProvider { override fun now(): Instant = currentInstant fun setNow(value: Instant) { currentInstant = value } } And a Given-When-Then aggregate test: @Test fun `should confirm draft order by owner`() { // given val order = Order.fromSnapshot( anOrder() .withId("ORD-123") .ownedBy("user-123") .build(), ) // and currentUserIs("user-123") // and currentTimeIs("2026-06-05T10:15:30Z") // when context(clock, domainEvents, currentUser) { order.confirm() } // then expectThat(order.toSnapshot()) .isConfirmed() .wasConfirmedAt("2026-06-05T10:15:30Z") // and expectThat(domainEvents) .hasPublishedOrderConfirmed( orderId = "ORD-123", confirmedAt = "2026-06-05T10:15:30Z", ) } This is still a normal Given-When-Then test. The test context is explicit: context(clock, domainEvents, currentUser) { order.confirm() } But the domain operation itself does not have a noisy parameter list. Where would I use it? For things that are execution context: context(clock: ClockProvider) fun Order.isExpired(): Boolean context(user: UserContext) fun Document.canBeEdited(): Boolean context(tx: Transaction) fun OrderRepository.save(order: Order) context(events: DomainEvents) fun Order.markAsPaid() These are not the main input data. They are also not always stable object dependencies. They are the environment in which a piece of logic makes sense. Where would I not use it? I would not put half of the application into context(...). ☹️ Sad code: context( user: UserContext, tenant: TenantContext, clock: ClockProvider, events: DomainEvents, logger: Logger, paymentGateway: PaymentGateway, orderRepository: OrderRepository, invoiceClient: InvoiceClient, emailSender: EmailSender, ) fun confirmOrder(orderId: OrderId) { // ... } This is not execution context. This is a DI container written with different syntax. I also would not put normal input data there: context(orderId: OrderId) fun confirmOrder() orderId is an operation argument. Not context. This is clearer: fun confirmOrder(orderId: OrderId) Not everything has to use the new syntax. Watch out for overly generic types Context is resolved by types, so I would be careful with primitives and generic types. ☹️ Sad code: context(id: String) fun loadSomething() { // ... } Which string? User id? Tenant id? Correlation id? A small type is better: @JvmInline value class UserId(val value: String) @JvmInline value class TenantId(val value: String) data class TenantContext( val tenantId: TenantId, ) Good types matter even more with context parameters, because the compiler uses them to find the required context. Summary context parameters are not a replacement for DI. They do not create objects. They do not manage lifecycles. They do not replace constructors. The split that works best for me is: constructor injection -&gt; stable object dependencies normal parameters -&gt; operation input context parameters -&gt; execution context In a small domain fragment, this can clean up the code nicely. Not because parameters magically disappear. Because we name their role better. Example repository: kotlin-context-parameters-ddd Technical sources: Context parameters - Kotlin Documentation What’s new in Kotlin 2.4.0]]></summary></entry><entry xml:lang="pl"><title type="html">Kotlin context parameters: nie DI, tylko kontekst wykonania</title><link href="https://camilyed.github.io//pl/kotlin-context-parameters-nie-di-tylko-kontekst-wykonania/" rel="alternate" type="text/html" title="Kotlin context parameters: nie DI, tylko kontekst wykonania" /><published>2026-06-05T00:00:00+00:00</published><updated>2026-06-05T00:00:00+00:00</updated><id>https://camilyed.github.io//pl/kotlin-2-4-co-warto-sprawdzic-v2</id><content type="html" xml:base="https://camilyed.github.io//pl/kotlin-context-parameters-nie-di-tylko-kontekst-wykonania/"><![CDATA[<p>Kiedy zobaczyłem <code class="language-plaintext highlighter-rouge">context(...)</code> przy funkcji, pierwsze skojarzenie było dość oczywiste:</p>

<blockquote>
  <p>czy Kotlin dostał kolejny sposób na dependency injection?</p>
</blockquote>

<p>Po kilku próbach patrzę na to inaczej.</p>

<p><code class="language-plaintext highlighter-rouge">context parameters</code> nie tworzą obiektów. Nie zarządzają cyklem życia zależności. Nie zastępują Springa, Koina ani Daggera.</p>

<p>One opisują coś prostszego:</p>

<blockquote>
  <p>ta funkcja może wykonać się tylko wtedy, gdy w miejscu wywołania istnieje wymagany kontekst.</p>
</blockquote>

<p>I ten warunek jest sprawdzany przez kompilator.</p>

<p>To jest dla mnie najważniejsze zdanie w całym temacie. Nie runtime. Nie magiczny provider. Nie globalny singleton. Kompilator.</p>

<h2 id="najprostszy-przykład">Najprostszy przykład</h2>

<p>Weźmy zegar.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">ClockProvider</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">now</span><span class="p">():</span> <span class="nc">Instant</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Jeśli funkcja potrzebuje aktualnego czasu, zwykle mamy trzy opcje.</p>

<p>Możemy przekazać zegar jako zwykły parametr:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">confirmedAt</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">):</span> <span class="nc">Instant</span> <span class="p">=</span>
    <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
</code></pre></div></div>

<p>Możemy trzymać go w konstruktorze jak klasyczne dependency injection:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">OrderService</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">confirmedAt</span><span class="p">():</span> <span class="nc">Instant</span> <span class="p">=</span>
        <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Albo możemy powiedzieć, że zegar jest kontekstem wykonania tej funkcji:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirmedAt</span><span class="p">():</span> <span class="nc">Instant</span> <span class="p">=</span>
    <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
</code></pre></div></div>

<p>Wywołanie:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">clock</span> <span class="p">=</span> <span class="nc">FixedClockProvider</span><span class="p">(</span>
    <span class="nc">Instant</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="s">"2026-06-05T10:15:30Z"</span><span class="p">),</span>
<span class="p">)</span>

<span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">confirmedAt</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Bez <code class="language-plaintext highlighter-rouge">context(clock)</code> ta funkcja nie powinna się skompilować.</p>

<p>I to jest różnica względem service locatora. Zależność nadal jest widoczna w sygnaturze funkcji, ale nie przepychamy jej jako zwykłego argumentu przez każde wywołanie.</p>

<h2 id="parametr-konstruktor-czy-context">Parametr, konstruktor czy context?</h2>

<p>Nie traktowałbym <code class="language-plaintext highlighter-rouge">context parameters</code> jako nowej domyślnej odpowiedzi na wszystko.</p>

<p>Dla mnie podział jest mniej więcej taki:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>zwykłe parametry      -&gt; dane wejściowe operacji
constructor injection -&gt; stałe zależności obiektu
context parameters    -&gt; kontekst wykonania
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">orderId</code>, <code class="language-plaintext highlighter-rouge">amount</code>, <code class="language-plaintext highlighter-rouge">email</code>, <code class="language-plaintext highlighter-rouge">newAddress</code> — to są normalne parametry.</p>

<p><code class="language-plaintext highlighter-rouge">OrderRepository</code>, <code class="language-plaintext highlighter-rouge">PaymentGateway</code>, <code class="language-plaintext highlighter-rouge">HttpClient</code> — często pasują do konstruktora use case’a albo serwisu.</p>

<p><code class="language-plaintext highlighter-rouge">ClockProvider</code>, <code class="language-plaintext highlighter-rouge">UserContext</code>, <code class="language-plaintext highlighter-rouge">DomainEvents</code>, <code class="language-plaintext highlighter-rouge">Transaction</code>, <code class="language-plaintext highlighter-rouge">TenantContext</code>, <code class="language-plaintext highlighter-rouge">CorrelationId</code> — to są dobrzy kandydaci na kontekst wykonania.</p>

<p>Oczywiście nie zawsze. Ale ta granica jest dla mnie dużo zdrowsza niż myślenie: „super, teraz wszystko wrzucamy w <code class="language-plaintext highlighter-rouge">context(...)</code>”.</p>

<h2 id="przykład-z-repo">Przykład z repo</h2>

<p>Do artykułu zrobiłem małe repo z przykładem DDD / hexagonal:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/main/kotlin/io/github/camilyed/contextparameters
├── domain
├── application
└── adapter
</code></pre></div></div>

<p>Nie ma tutaj Springa. Celowo.</p>

<p>Chciałem, żeby przykład pokazywał samą składnię i decyzję projektową, a nie integrację z frameworkiem.</p>

<p>Domena ma jeden agregat: <code class="language-plaintext highlighter-rouge">Order</code>.</p>

<p>Zamówienie może być szkicem albo może być potwierdzone:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">sealed</span> <span class="kd">interface</span> <span class="nc">OrderState</span> <span class="p">{</span>

    <span class="n">data</span> <span class="kd">object</span> <span class="nc">Draft</span> <span class="p">:</span> <span class="nc">OrderState</span>

    <span class="kd">data class</span> <span class="nc">Confirmed</span><span class="p">(</span>
        <span class="kd">val</span> <span class="py">confirmedAt</span><span class="p">:</span> <span class="nc">Instant</span><span class="p">,</span>
    <span class="p">)</span> <span class="p">:</span> <span class="nc">OrderState</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Snapshot agregatu jest prosty:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">OrderSnapshot</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">ownerId</span><span class="p">:</span> <span class="nc">UserId</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">state</span><span class="p">:</span> <span class="nc">OrderState</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>I teraz sama reguła.</p>

<p>Zamówienie można potwierdzić tylko wtedy, gdy:</p>

<ul>
  <li>jest jeszcze szkicem,</li>
  <li>aktualny użytkownik jest właścicielem albo może potwierdzać zamówienia,</li>
  <li>zapisujemy czas potwierdzenia,</li>
  <li>publikujemy event domenowy.</li>
</ul>

<p>W agregacie wygląda to tak:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Order</span> <span class="k">private</span> <span class="k">constructor</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">,</span>
    <span class="kd">val</span> <span class="py">ownerId</span><span class="p">:</span> <span class="nc">UserId</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">var</span> <span class="py">state</span><span class="p">:</span> <span class="nc">OrderState</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
    <span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">require</span><span class="p">(</span><span class="n">state</span> <span class="p">==</span> <span class="nc">OrderState</span><span class="p">.</span><span class="nc">Draft</span><span class="p">)</span> <span class="p">{</span>
            <span class="s">"Only draft order can be confirmed"</span>
        <span class="p">}</span>

        <span class="nf">require</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">userId</span> <span class="p">==</span> <span class="n">ownerId</span> <span class="p">||</span> <span class="n">user</span><span class="p">.</span><span class="n">canConfirmOrders</span><span class="p">)</span> <span class="p">{</span>
            <span class="s">"User cannot confirm this order"</span>
        <span class="p">}</span>

        <span class="kd">val</span> <span class="py">now</span> <span class="p">=</span> <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>

        <span class="n">state</span> <span class="p">=</span> <span class="nc">OrderState</span><span class="p">.</span><span class="nc">Confirmed</span><span class="p">(</span>
            <span class="n">confirmedAt</span> <span class="p">=</span> <span class="n">now</span><span class="p">,</span>
        <span class="p">)</span>

        <span class="n">events</span><span class="p">.</span><span class="nf">publish</span><span class="p">(</span>
            <span class="nc">OrderConfirmed</span><span class="p">(</span>
                <span class="n">orderId</span> <span class="p">=</span> <span class="n">id</span><span class="p">,</span>
                <span class="n">confirmedAt</span> <span class="p">=</span> <span class="n">now</span><span class="p">,</span>
            <span class="p">),</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To jest zwykła metoda domenowa.</p>

<p>Ale jej sygnatura mówi coś ważnego:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span>
</code></pre></div></div>

<p>Czyli:</p>

<blockquote>
  <p>potrafię potwierdzić zamówienie, ale tylko w kontekście zegara, eventów domenowych i aktualnego użytkownika.</p>
</blockquote>

<p>Nie chcę trzymać <code class="language-plaintext highlighter-rouge">ClockProvider</code> w konstruktorze encji. Zegar nie opisuje zamówienia.</p>

<p>Nie chcę też chować użytkownika w globalnym <code class="language-plaintext highlighter-rouge">CurrentUserProvider</code>.</p>

<p>Ta operacja po prostu działa w konkretnym kontekście.</p>

<h2 id="warstwa-aplikacji">Warstwa aplikacji</h2>

<p>W use case dochodzi jeszcze transakcja.</p>

<p>I tutaj przykład robi się ciekawszy, bo widać podział odpowiedzialności:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="nn">io.github.camilyed.contextparameters.application</span>

<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.ClockProvider</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.DomainEvents</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.OrderRepository</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.OrderSnapshot</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.Transaction</span>
<span class="k">import</span> <span class="nn">io.github.camilyed.contextparameters.domain.UserContext</span>

<span class="kd">class</span> <span class="nc">ConfirmOrderUseCase</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">orderRepository</span><span class="p">:</span> <span class="nc">OrderRepository</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="nf">context</span><span class="p">(</span>
        <span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span>
        <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span>
        <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">,</span>
        <span class="n">transaction</span><span class="p">:</span> <span class="nc">Transaction</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">fun</span> <span class="nf">confirm</span><span class="p">(</span><span class="n">command</span><span class="p">:</span> <span class="nc">ConfirmOrderCommand</span><span class="p">):</span> <span class="nc">OrderSnapshot</span> <span class="p">=</span>
        <span class="n">transaction</span><span class="p">.</span><span class="nf">within</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">order</span> <span class="p">=</span> <span class="n">orderRepository</span><span class="p">.</span><span class="nf">findById</span><span class="p">(</span><span class="n">command</span><span class="p">.</span><span class="n">orderId</span><span class="p">)</span>

            <span class="n">order</span><span class="p">.</span><span class="nf">confirm</span><span class="p">()</span>

            <span class="n">orderRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">)</span>

            <span class="n">order</span><span class="p">.</span><span class="nf">toSnapshot</span><span class="p">()</span>
        <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">orderRepository</code> jest w konstruktorze, bo to stała zależność use case’a.</p>

<p><code class="language-plaintext highlighter-rouge">command</code> jest zwykłym parametrem, bo to dane wejściowe operacji.</p>

<p><code class="language-plaintext highlighter-rouge">clock</code>, <code class="language-plaintext highlighter-rouge">events</code>, <code class="language-plaintext highlighter-rouge">user</code> i <code class="language-plaintext highlighter-rouge">transaction</code> są w <code class="language-plaintext highlighter-rouge">context(...)</code>, bo opisują otoczenie, w którym wykonuje się operacja.</p>

<p>Ten podział jest dla mnie najczytelniejszy.</p>

<h2 id="a-gdzie-adaptery">A gdzie adaptery?</h2>

<p>Interfejsy są w domenie:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">OrderRepository</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">findById</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">):</span> <span class="nc">Order</span>
    <span class="k">fun</span> <span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">:</span> <span class="nc">Order</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Adapter daje implementację:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">InMemoryOrderRepository</span> <span class="p">:</span> <span class="nc">OrderRepository</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">orders</span> <span class="p">=</span> <span class="n">mutableMapOf</span><span class="p">&lt;</span><span class="nc">OrderId</span><span class="p">,</span> <span class="nc">Order</span><span class="p">&gt;()</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">findById</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">):</span> <span class="nc">Order</span> <span class="p">=</span>
        <span class="n">orders</span><span class="p">[</span><span class="n">id</span><span class="p">]</span>
            <span class="o">?:</span> <span class="k">throw</span> <span class="nc">OrderNotFoundException</span><span class="p">(</span><span class="s">"Order with id ${id.value} not found"</span><span class="p">)</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">:</span> <span class="nc">Order</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">orders</span><span class="p">[</span><span class="n">order</span><span class="p">.</span><span class="n">id</span><span class="p">]</span> <span class="p">=</span> <span class="n">order</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Czyli klasyczny kierunek zależności zostaje zachowany.</p>

<p>Domena zna abstrakcję.</p>

<p>Adapter zna implementację.</p>

<p>Use case dostaje repozytorium przez konstruktor, a kontekst wykonania przez <code class="language-plaintext highlighter-rouge">context parameters</code>.</p>

<h2 id="dlaczego-nie-service-locator">Dlaczego nie Service Locator?</h2>

<p>Bo Service Locator usuwa zależności z sygnatury.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">clock</span> <span class="p">=</span> <span class="nc">ServiceLocator</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">ClockProvider</span><span class="p">&gt;()</span>
    <span class="kd">val</span> <span class="py">events</span> <span class="p">=</span> <span class="nc">ServiceLocator</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">DomainEvents</span><span class="p">&gt;()</span>
    <span class="kd">val</span> <span class="py">user</span> <span class="p">=</span> <span class="nc">ServiceLocator</span><span class="p">.</span><span class="k">get</span><span class="p">&lt;</span><span class="nc">UserContext</span><span class="p">&gt;()</span>

    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Na pierwszy rzut oka metoda wygląda czyściej.</p>

<p>Ale to jest czystość na kredyt.</p>

<p>Czytając sygnaturę <code class="language-plaintext highlighter-rouge">confirm()</code>, nie wiemy już, czego ta metoda potrzebuje. Trzeba wejść do środka. A jeśli czegoś zabraknie w locatorze, często dowiemy się o tym dopiero w runtime.</p>

<p>Przy <code class="language-plaintext highlighter-rouge">context parameters</code> zależność nadal jest częścią kontraktu:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirm</span><span class="p">()</span>
</code></pre></div></div>

<p>Czytelnik widzi wymagania.</p>

<p>Kompilator ich pilnuje.</p>

<h2 id="test-bez-kontenera">Test bez kontenera</h2>

<p>W testach nie potrzebuję Springa ani kontenera DI.</p>

<p>Mam fake zegara:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">TestingClockProvider</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">var</span> <span class="py">currentInstant</span><span class="p">:</span> <span class="nc">Instant</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">ClockProvider</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">now</span><span class="p">():</span> <span class="nc">Instant</span> <span class="p">=</span>
        <span class="n">currentInstant</span>

    <span class="k">fun</span> <span class="nf">setNow</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nc">Instant</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">currentInstant</span> <span class="p">=</span> <span class="n">value</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I test zachowania agregatu:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`should</span> <span class="n">confirm</span> <span class="n">draft</span> <span class="n">order</span> <span class="k">by</span> <span class="nf">owner`</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// given</span>
    <span class="kd">val</span> <span class="py">order</span> <span class="p">=</span>
        <span class="nc">Order</span><span class="p">.</span><span class="nf">fromSnapshot</span><span class="p">(</span>
            <span class="nf">anOrder</span><span class="p">()</span>
                <span class="p">.</span><span class="nf">withId</span><span class="p">(</span><span class="s">"ORD-123"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">ownedBy</span><span class="p">(</span><span class="s">"user-123"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">build</span><span class="p">(),</span>
        <span class="p">)</span>

    <span class="c1">// and</span>
    <span class="nf">currentUserIs</span><span class="p">(</span><span class="s">"user-123"</span><span class="p">)</span>

    <span class="c1">// and</span>
    <span class="nf">currentTimeIs</span><span class="p">(</span><span class="s">"2026-06-05T10:15:30Z"</span><span class="p">)</span>

    <span class="c1">// when</span>
    <span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">,</span> <span class="n">domainEvents</span><span class="p">,</span> <span class="n">currentUser</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">order</span><span class="p">.</span><span class="nf">confirm</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="c1">// then</span>
    <span class="nf">expectThat</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="nf">toSnapshot</span><span class="p">())</span>
        <span class="p">.</span><span class="nf">isConfirmed</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">wasConfirmedAt</span><span class="p">(</span><span class="s">"2026-06-05T10:15:30Z"</span><span class="p">)</span>

    <span class="c1">// and</span>
    <span class="nf">expectThat</span><span class="p">(</span><span class="n">domainEvents</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">hasPublishedOrderConfirmed</span><span class="p">(</span>
            <span class="n">orderId</span> <span class="p">=</span> <span class="s">"ORD-123"</span><span class="p">,</span>
            <span class="n">confirmedAt</span> <span class="p">=</span> <span class="s">"2026-06-05T10:15:30Z"</span><span class="p">,</span>
        <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To jest nadal zwykły test Given-When-Then.</p>

<p>Kontekst testu jest jawny:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">,</span> <span class="n">domainEvents</span><span class="p">,</span> <span class="n">currentUser</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">order</span><span class="p">.</span><span class="nf">confirm</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Ale sama operacja domenowa nie ma sztucznej listy parametrów.</p>

<h2 id="kiedy-bym-tego-użył">Kiedy bym tego użył?</h2>

<p>Przy rzeczach, które są kontekstem wykonania:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">Order</span><span class="p">.</span><span class="nf">isExpired</span><span class="p">():</span> <span class="nc">Boolean</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">Document</span><span class="p">.</span><span class="nf">canBeEdited</span><span class="p">():</span> <span class="nc">Boolean</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">tx</span><span class="p">:</span> <span class="nc">Transaction</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">OrderRepository</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">order</span><span class="p">:</span> <span class="nc">Order</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">)</span>
<span class="k">fun</span> <span class="nc">Order</span><span class="p">.</span><span class="nf">markAsPaid</span><span class="p">()</span>
</code></pre></div></div>

<p>To nie są główne dane wejściowe.</p>

<p>To nie są też zawsze stałe zależności obiektu.</p>

<p>To jest otoczenie, w którym dana logika ma sens.</p>

<h2 id="kiedy-bym-tego-nie-użył">Kiedy bym tego nie użył?</h2>

<p>Nie wrzucałbym do <code class="language-plaintext highlighter-rouge">context(...)</code> połowy aplikacji.</p>

<p>☹️ Smutny kodzik:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span>
    <span class="n">user</span><span class="p">:</span> <span class="nc">UserContext</span><span class="p">,</span>
    <span class="n">tenant</span><span class="p">:</span> <span class="nc">TenantContext</span><span class="p">,</span>
    <span class="n">clock</span><span class="p">:</span> <span class="nc">ClockProvider</span><span class="p">,</span>
    <span class="n">events</span><span class="p">:</span> <span class="nc">DomainEvents</span><span class="p">,</span>
    <span class="n">logger</span><span class="p">:</span> <span class="nc">Logger</span><span class="p">,</span>
    <span class="n">paymentGateway</span><span class="p">:</span> <span class="nc">PaymentGateway</span><span class="p">,</span>
    <span class="n">orderRepository</span><span class="p">:</span> <span class="nc">OrderRepository</span><span class="p">,</span>
    <span class="n">invoiceClient</span><span class="p">:</span> <span class="nc">InvoiceClient</span><span class="p">,</span>
    <span class="n">emailSender</span><span class="p">:</span> <span class="nc">EmailSender</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirmOrder</span><span class="p">(</span><span class="n">orderId</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To nie jest kontekst wykonania.</p>

<p>To jest kontener DI zapisany inną składnią.</p>

<p>Nie dawałbym tam też zwykłych danych wejściowych:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">orderId</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">confirmOrder</span><span class="p">()</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">orderId</code> jest argumentem operacji.</p>

<p>Nie kontekstem.</p>

<p>Czytelniej:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">confirmOrder</span><span class="p">(</span><span class="n">orderId</span><span class="p">:</span> <span class="nc">OrderId</span><span class="p">)</span>
</code></pre></div></div>

<p>Nie wszystko musi używać nowej składni.</p>

<h2 id="uwaga-na-zbyt-ogólne-typy">Uwaga na zbyt ogólne typy</h2>

<p>Context jest rozwiązywany po typach, więc warto uważać na prymitywy i zbyt ogólne typy.</p>

<p>☹️ Smutny kodzik:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">context</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
<span class="k">fun</span> <span class="nf">loadSomething</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Jaki string?</p>

<p>Id użytkownika?</p>

<p>Id tenanta?</p>

<p>Correlation id?</p>

<p>Lepiej użyć małych typów:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@JvmInline</span>
<span class="n">value</span> <span class="kd">class</span> <span class="nc">UserId</span><span class="p">(</span><span class="kd">val</span> <span class="py">value</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>

<span class="nd">@JvmInline</span>
<span class="n">value</span> <span class="kd">class</span> <span class="nc">TenantId</span><span class="p">(</span><span class="kd">val</span> <span class="py">value</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>

<span class="kd">data class</span> <span class="nc">TenantContext</span><span class="p">(</span>
    <span class="kd">val</span> <span class="py">tenantId</span><span class="p">:</span> <span class="nc">TenantId</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Przy <code class="language-plaintext highlighter-rouge">context parameters</code> dobre typy są jeszcze ważniejsze, bo to po nich kompilator rozpoznaje, czego szuka.</p>

<h2 id="podsumowanie">Podsumowanie</h2>

<p><code class="language-plaintext highlighter-rouge">context parameters</code> nie są zamiennikiem DI.</p>

<p>Nie tworzą obiektów.</p>

<p>Nie zarządzają cyklem życia.</p>

<p>Nie zastępują konstruktora.</p>

<p>Najprostszy podział, który mi się sprawdza:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>constructor injection -&gt; stałe zależności obiektu
zwykłe parametry      -&gt; dane wejściowe operacji
context parameters    -&gt; kontekst wykonania
</code></pre></div></div>

<p>W małym fragmencie domeny to potrafi fajnie oczyścić kod.</p>

<p>Nie dlatego, że parametry magicznie znikają.</p>

<p>Tylko dlatego, że lepiej nazywamy ich rolę.</p>

<p>Przykładowe repo: <a href="https://github.com/CamilYed/kotlin-context-parameters-ddd">kotlin-context-parameters-ddd</a></p>

<p>Źródła techniczne:</p>

<ul>
  <li><a href="https://kotlinlang.org/docs/context-parameters.html">Context parameters - Kotlin Documentation</a></li>
  <li><a href="https://kotlinlang.org/docs/whatsnew24.html">What’s new in Kotlin 2.4.0</a></li>
</ul>]]></content><author><name></name></author><category term="kotlin" /><category term="ddd" /><category term="testing" /><category term="software-engineering" /><summary type="html"><![CDATA[Kiedy zobaczyłem context(...) przy funkcji, pierwsze skojarzenie było dość oczywiste: czy Kotlin dostał kolejny sposób na dependency injection? Po kilku próbach patrzę na to inaczej. context parameters nie tworzą obiektów. Nie zarządzają cyklem życia zależności. Nie zastępują Springa, Koina ani Daggera. One opisują coś prostszego: ta funkcja może wykonać się tylko wtedy, gdy w miejscu wywołania istnieje wymagany kontekst. I ten warunek jest sprawdzany przez kompilator. To jest dla mnie najważniejsze zdanie w całym temacie. Nie runtime. Nie magiczny provider. Nie globalny singleton. Kompilator. Najprostszy przykład Weźmy zegar. interface ClockProvider { fun now(): Instant } Jeśli funkcja potrzebuje aktualnego czasu, zwykle mamy trzy opcje. Możemy przekazać zegar jako zwykły parametr: fun confirmedAt(clock: ClockProvider): Instant = clock.now() Możemy trzymać go w konstruktorze jak klasyczne dependency injection: class OrderService( private val clock: ClockProvider, ) { fun confirmedAt(): Instant = clock.now() } Albo możemy powiedzieć, że zegar jest kontekstem wykonania tej funkcji: context(clock: ClockProvider) fun confirmedAt(): Instant = clock.now() Wywołanie: val clock = FixedClockProvider( Instant.parse("2026-06-05T10:15:30Z"), ) context(clock) { confirmedAt() } Bez context(clock) ta funkcja nie powinna się skompilować. I to jest różnica względem service locatora. Zależność nadal jest widoczna w sygnaturze funkcji, ale nie przepychamy jej jako zwykłego argumentu przez każde wywołanie. Parametr, konstruktor czy context? Nie traktowałbym context parameters jako nowej domyślnej odpowiedzi na wszystko. Dla mnie podział jest mniej więcej taki: zwykłe parametry -&gt; dane wejściowe operacji constructor injection -&gt; stałe zależności obiektu context parameters -&gt; kontekst wykonania orderId, amount, email, newAddress — to są normalne parametry. OrderRepository, PaymentGateway, HttpClient — często pasują do konstruktora use case’a albo serwisu. ClockProvider, UserContext, DomainEvents, Transaction, TenantContext, CorrelationId — to są dobrzy kandydaci na kontekst wykonania. Oczywiście nie zawsze. Ale ta granica jest dla mnie dużo zdrowsza niż myślenie: „super, teraz wszystko wrzucamy w context(...)”. Przykład z repo Do artykułu zrobiłem małe repo z przykładem DDD / hexagonal: src/main/kotlin/io/github/camilyed/contextparameters ├── domain ├── application └── adapter Nie ma tutaj Springa. Celowo. Chciałem, żeby przykład pokazywał samą składnię i decyzję projektową, a nie integrację z frameworkiem. Domena ma jeden agregat: Order. Zamówienie może być szkicem albo może być potwierdzone: sealed interface OrderState { data object Draft : OrderState data class Confirmed( val confirmedAt: Instant, ) : OrderState } Snapshot agregatu jest prosty: data class OrderSnapshot( val id: OrderId, val ownerId: UserId, val state: OrderState, ) I teraz sama reguła. Zamówienie można potwierdzić tylko wtedy, gdy: jest jeszcze szkicem, aktualny użytkownik jest właścicielem albo może potwierdzać zamówienia, zapisujemy czas potwierdzenia, publikujemy event domenowy. W agregacie wygląda to tak: class Order private constructor( val id: OrderId, val ownerId: UserId, private var state: OrderState, ) { context(clock: ClockProvider, events: DomainEvents, user: UserContext) fun confirm() { require(state == OrderState.Draft) { "Only draft order can be confirmed" } require(user.userId == ownerId || user.canConfirmOrders) { "User cannot confirm this order" } val now = clock.now() state = OrderState.Confirmed( confirmedAt = now, ) events.publish( OrderConfirmed( orderId = id, confirmedAt = now, ), ) } } To jest zwykła metoda domenowa. Ale jej sygnatura mówi coś ważnego: context(clock: ClockProvider, events: DomainEvents, user: UserContext) fun confirm() Czyli: potrafię potwierdzić zamówienie, ale tylko w kontekście zegara, eventów domenowych i aktualnego użytkownika. Nie chcę trzymać ClockProvider w konstruktorze encji. Zegar nie opisuje zamówienia. Nie chcę też chować użytkownika w globalnym CurrentUserProvider. Ta operacja po prostu działa w konkretnym kontekście. Warstwa aplikacji W use case dochodzi jeszcze transakcja. I tutaj przykład robi się ciekawszy, bo widać podział odpowiedzialności: package io.github.camilyed.contextparameters.application import io.github.camilyed.contextparameters.domain.ClockProvider import io.github.camilyed.contextparameters.domain.DomainEvents import io.github.camilyed.contextparameters.domain.OrderRepository import io.github.camilyed.contextparameters.domain.OrderSnapshot import io.github.camilyed.contextparameters.domain.Transaction import io.github.camilyed.contextparameters.domain.UserContext class ConfirmOrderUseCase( private val orderRepository: OrderRepository, ) { context( clock: ClockProvider, events: DomainEvents, user: UserContext, transaction: Transaction, ) fun confirm(command: ConfirmOrderCommand): OrderSnapshot = transaction.within { val order = orderRepository.findById(command.orderId) order.confirm() orderRepository.save(order) order.toSnapshot() } } orderRepository jest w konstruktorze, bo to stała zależność use case’a. command jest zwykłym parametrem, bo to dane wejściowe operacji. clock, events, user i transaction są w context(...), bo opisują otoczenie, w którym wykonuje się operacja. Ten podział jest dla mnie najczytelniejszy. A gdzie adaptery? Interfejsy są w domenie: interface OrderRepository { fun findById(id: OrderId): Order fun save(order: Order) } Adapter daje implementację: class InMemoryOrderRepository : OrderRepository { private val orders = mutableMapOf&lt;OrderId, Order&gt;() override fun findById(id: OrderId): Order = orders[id] ?: throw OrderNotFoundException("Order with id ${id.value} not found") override fun save(order: Order) { orders[order.id] = order } } Czyli klasyczny kierunek zależności zostaje zachowany. Domena zna abstrakcję. Adapter zna implementację. Use case dostaje repozytorium przez konstruktor, a kontekst wykonania przez context parameters. Dlaczego nie Service Locator? Bo Service Locator usuwa zależności z sygnatury. fun confirm() { val clock = ServiceLocator.get&lt;ClockProvider&gt;() val events = ServiceLocator.get&lt;DomainEvents&gt;() val user = ServiceLocator.get&lt;UserContext&gt;() // ... } Na pierwszy rzut oka metoda wygląda czyściej. Ale to jest czystość na kredyt. Czytając sygnaturę confirm(), nie wiemy już, czego ta metoda potrzebuje. Trzeba wejść do środka. A jeśli czegoś zabraknie w locatorze, często dowiemy się o tym dopiero w runtime. Przy context parameters zależność nadal jest częścią kontraktu: context(clock: ClockProvider, events: DomainEvents, user: UserContext) fun confirm() Czytelnik widzi wymagania. Kompilator ich pilnuje. Test bez kontenera W testach nie potrzebuję Springa ani kontenera DI. Mam fake zegara: class TestingClockProvider( private var currentInstant: Instant, ) : ClockProvider { override fun now(): Instant = currentInstant fun setNow(value: Instant) { currentInstant = value } } I test zachowania agregatu: @Test fun `should confirm draft order by owner`() { // given val order = Order.fromSnapshot( anOrder() .withId("ORD-123") .ownedBy("user-123") .build(), ) // and currentUserIs("user-123") // and currentTimeIs("2026-06-05T10:15:30Z") // when context(clock, domainEvents, currentUser) { order.confirm() } // then expectThat(order.toSnapshot()) .isConfirmed() .wasConfirmedAt("2026-06-05T10:15:30Z") // and expectThat(domainEvents) .hasPublishedOrderConfirmed( orderId = "ORD-123", confirmedAt = "2026-06-05T10:15:30Z", ) } To jest nadal zwykły test Given-When-Then. Kontekst testu jest jawny: context(clock, domainEvents, currentUser) { order.confirm() } Ale sama operacja domenowa nie ma sztucznej listy parametrów. Kiedy bym tego użył? Przy rzeczach, które są kontekstem wykonania: context(clock: ClockProvider) fun Order.isExpired(): Boolean context(user: UserContext) fun Document.canBeEdited(): Boolean context(tx: Transaction) fun OrderRepository.save(order: Order) context(events: DomainEvents) fun Order.markAsPaid() To nie są główne dane wejściowe. To nie są też zawsze stałe zależności obiektu. To jest otoczenie, w którym dana logika ma sens. Kiedy bym tego nie użył? Nie wrzucałbym do context(...) połowy aplikacji. ☹️ Smutny kodzik: context( user: UserContext, tenant: TenantContext, clock: ClockProvider, events: DomainEvents, logger: Logger, paymentGateway: PaymentGateway, orderRepository: OrderRepository, invoiceClient: InvoiceClient, emailSender: EmailSender, ) fun confirmOrder(orderId: OrderId) { // ... } To nie jest kontekst wykonania. To jest kontener DI zapisany inną składnią. Nie dawałbym tam też zwykłych danych wejściowych: context(orderId: OrderId) fun confirmOrder() orderId jest argumentem operacji. Nie kontekstem. Czytelniej: fun confirmOrder(orderId: OrderId) Nie wszystko musi używać nowej składni. Uwaga na zbyt ogólne typy Context jest rozwiązywany po typach, więc warto uważać na prymitywy i zbyt ogólne typy. ☹️ Smutny kodzik: context(id: String) fun loadSomething() { // ... } Jaki string? Id użytkownika? Id tenanta? Correlation id? Lepiej użyć małych typów: @JvmInline value class UserId(val value: String) @JvmInline value class TenantId(val value: String) data class TenantContext( val tenantId: TenantId, ) Przy context parameters dobre typy są jeszcze ważniejsze, bo to po nich kompilator rozpoznaje, czego szuka. Podsumowanie context parameters nie są zamiennikiem DI. Nie tworzą obiektów. Nie zarządzają cyklem życia. Nie zastępują konstruktora. Najprostszy podział, który mi się sprawdza: constructor injection -&gt; stałe zależności obiektu zwykłe parametry -&gt; dane wejściowe operacji context parameters -&gt; kontekst wykonania W małym fragmencie domeny to potrafi fajnie oczyścić kod. Nie dlatego, że parametry magicznie znikają. Tylko dlatego, że lepiej nazywamy ich rolę. Przykładowe repo: kotlin-context-parameters-ddd Źródła techniczne: Context parameters - Kotlin Documentation What’s new in Kotlin 2.4.0]]></summary></entry><entry xml:lang="en"><title type="html">Tests That Don’t Lie, Part 2: The Mockito Trap and In-Memory Implementations</title><link href="https://camilyed.github.io//en/tests-that-dont-lie-part-2/" rel="alternate" type="text/html" title="Tests That Don’t Lie, Part 2: The Mockito Trap and In-Memory Implementations" /><published>2026-01-31T00:00:00+00:00</published><updated>2026-01-31T00:00:00+00:00</updated><id>https://camilyed.github.io//en/tests-that-dont-lie-mockito-trap-in-memory</id><content type="html" xml:base="https://camilyed.github.io//en/tests-that-dont-lie-part-2/"><![CDATA[<p>In the previous part, we focused on how to write tests that are simply pleasant to read. But readability is only half the
battle. You can have the most beautifully written Given-When-Then section that… checks absolutely nothing.</p>

<p>Today we will talk about trust in our tests. Because the worst kind of test is one that gives you a sense of safety,
even though the code underneath does something completely different from what the test suggests.</p>

<h3 id="1-the-trap-of-testing-implementation-white-box">1. The trap of testing implementation (White Box)</h3>

<p>I have noticed that in many projects Mockito is added to tests “automatically”. We generate a test class, mock all
dependencies, and done. Few people ask themselves then: <strong>why am I actually using this mock?</strong></p>

<p>Imagine a simple service for updating user data.</p>

<p>☹️ <strong>Sad code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">userId</span> <span class="o">=</span> <span class="mi">1L</span><span class="o">;</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan"</span><span class="o">);</span>

    <span class="c1">// We have to "feed" the mock so the test can even start</span>
    <span class="n">when</span><span class="o">(</span><span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">)).</span><span class="na">thenReturn</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">user</span><span class="o">));</span>

    <span class="c1">// when</span>
    <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="c1">// We only check a technical method call.</span>
    <span class="c1">// Do we know whether the name was actually changed in the object before saving?</span>
    <span class="c1">// This test will say "YES" even if the service sends old data to save().</span>
    <span class="n">verify</span><span class="o">(</span><span class="n">userRepository</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">any</span><span class="o">(</span><span class="nc">User</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This test lies to you. It only checks whether the <code class="language-plaintext highlighter-rouge">save</code> method was called. If a developer mixes up fields and assigns
the new value to a completely different field in production code, or skips the assignment entirely, this test will still
be green. Instead of testing business behavior, which is changing the name, you test a technical library call.</p>

<p>Okay, but someone may notice that we can still verify the object state and try to use <code class="language-plaintext highlighter-rouge">ArgumentCaptor</code> for update logic.</p>

<p>☹️ Even sadder code:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldUpdateUserName_CaptorVersion</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">userId</span> <span class="o">=</span> <span class="mi">1L</span><span class="o">;</span>
    <span class="kt">var</span> <span class="n">existingUser</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan"</span><span class="o">);</span>
    <span class="kt">var</span> <span class="n">userCaptor</span> <span class="o">=</span> <span class="nc">ArgumentCaptor</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">User</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="n">when</span><span class="o">(</span><span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">)).</span><span class="na">thenReturn</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">existingUser</span><span class="o">));</span>

    <span class="c1">// when</span>
    <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="c1">// This is where the trouble begins. We expose implementation details.</span>
    <span class="n">verify</span><span class="o">(</span><span class="n">userRepository</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">userCaptor</span><span class="o">.</span><span class="na">capture</span><span class="o">());</span>
    <span class="kt">var</span> <span class="n">savedUser</span> <span class="o">=</span> <span class="n">userCaptor</span><span class="o">.</span><span class="na">getValue</span><span class="o">();</span>

    <span class="n">assertThat</span><span class="o">(</span><span class="n">savedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>So, success? Not exactly. We have just entered <code class="language-plaintext highlighter-rouge">White Box Testing</code> mode. Tests become fragile (<code class="language-plaintext highlighter-rouge">Fragile tests</code>) because:</p>

<ul>
  <li>Refactoring becomes painful: change <code class="language-plaintext highlighter-rouge">save()</code> to <code class="language-plaintext highlighter-rouge">saveAll()</code> and the test blows up, even though the business logic still
works.</li>
  <li>You test “how”, not “what”: you care whether a specific line of code was called, not what the result is for the user.</li>
  <li>Sonar lies: reports show line coverage, but you did not test those lines — you only executed them in an artificial
environment.</li>
</ul>

<h4 id="solution-in-memory-implementation">Solution: In-Memory implementation</h4>

<p>Instead of fighting Mockito, let’s treat the service as a black box. We need something that pretends to be a database but
works in memory. A <code class="language-plaintext highlighter-rouge">ConcurrentHashMap</code> under the repository is the simplest approach.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">InMemoryUserRepository</span> <span class="kd">implements</span> <span class="nc">UserRepository</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">,</span> <span class="nc">User</span><span class="o">&gt;</span> <span class="n">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">save</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">db</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">user</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">db</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">id</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">clear</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">db</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>A state-based test could look like this. Now the test does not need any <code class="language-plaintext highlighter-rouge">verify</code>. We simply execute the action and check
whether the state in the “database” is correct.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserServiceTest</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">InMemoryUserRepository</span> <span class="n">userRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryUserRepository</span><span class="o">();</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">UserService</span> <span class="n">userService</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UserService</span><span class="o">(</span><span class="n">userRepository</span><span class="o">);</span>

    <span class="nd">@BeforeEach</span>
    <span class="kt">void</span> <span class="nf">setup</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// given</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan"</span><span class="o">));</span>

        <span class="c1">// when</span>
        <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

        <span class="c1">// then</span>
        <span class="kt">var</span> <span class="n">updatedUser</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">updatedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="what-about-dsl">What about DSL?</h4>

<p>Remember the first part? We can use those patterns to prepare the initial state even more cleanly. Instead of manually
calling <code class="language-plaintext highlighter-rouge">userRepository.save()</code> in the given section, we will use our “ability”.</p>

<p>Happy code:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="n">thereIsAUser</span><span class="o">(</span><span class="n">anUser</span><span class="o">().</span><span class="na">withId</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">withName</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">).</span><span class="na">build</span><span class="o">());</span>

    <span class="c1">// when</span>
    <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="kt">var</span> <span class="n">updatedUser</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updatedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Is this still White Box? Someone might say: “Wait, but in the assertion you call the repository!”. No. The difference is
fundamental:</p>

<ul>
  <li>In Mockito (Interaction): You ask, “Did you call the save method?”. If a developer changes the way the save works, the
test fails.</li>
  <li>In-Memory (State): You ask, “System, no matter how you did it, does this user have a new name?”.</li>
</ul>

<p>In the <code class="language-plaintext highlighter-rouge">Black Box</code> approach, we treat the Service + InMemoryRepo pair as one black box. We do not care how many times the
service “talked” to the repository. We care about the final effect.</p>

<h4 id="how-it-could-look-in-the-end">How it could look in the end</h4>

<p>You may wonder: where does <code class="language-plaintext highlighter-rouge">Ability</code> get the repository from and is it definitely the same instance that the service
uses? This is the key point. For this to work, we need one source of truth.</p>

<p>The best way is to use interfaces with default methods.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserAbility</span> <span class="o">{</span>
    <span class="nc">UserRepository</span> <span class="nf">userRepository</span><span class="o">();</span> <span class="c1">// Provider method</span>

    <span class="k">default</span> <span class="kt">void</span> <span class="nf">thereIsAUser</span><span class="o">(</span><span class="nc">UserBuilder</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">userRepository</span><span class="o">().</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// builder in another package, for example com.ourdomain.testing.dsl.builders</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserBuilder</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1L</span><span class="o">;</span> <span class="c1">// Default ID</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"Jan"</span><span class="o">;</span> <span class="c1">// Default name</span>
    <span class="c1">// ... other fields</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">UserBuilder</span> <span class="nf">anUser</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">UserBuilder</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withId</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">id</span> <span class="o">=</span> <span class="n">id</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">build</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">User</span><span class="o">(</span><span class="n">id</span><span class="o">,</span> <span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Let’s create a base class for unit tests to hide technical implementation details of our DSL.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">BaseUnitTest</span> <span class="kd">implements</span> <span class="nc">UserAbility</span> <span class="o">{</span>
    <span class="c1">// One shared instance for the service, assertions, and all Abilities</span>
    <span class="kd">protected</span> <span class="kd">final</span> <span class="nc">InMemoryUserRepository</span> <span class="n">userRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryUserRepository</span><span class="o">();</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">UserRepository</span> <span class="nf">userRepository</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">userRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@BeforeEach</span>
    <span class="kt">void</span> <span class="nf">clearDatabase</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span> <span class="c1">// Important for isolating tests: clean in-memory database state before each test</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Thanks to this, your test class simply extends <code class="language-plaintext highlighter-rouge">BaseUnitTest</code> and can use our In-Memory implementation. Here is what the
final test class looks like. Notice how little technical noise is left. We focus only on business behavior.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserServiceTest</span> <span class="kd">extends</span> <span class="nc">BaseUnitTest</span> <span class="o">{</span>

    <span class="c1">// We inject the same repository that lives in BaseUnitTest</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">UserService</span> <span class="n">userService</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UserService</span><span class="o">(</span><span class="n">userRepository</span><span class="o">);</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// given</span>
        <span class="n">thereIsAUser</span><span class="o">(</span><span class="n">anUser</span><span class="o">().</span><span class="na">withId</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">withName</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">));</span>

        <span class="c1">// when</span>
        <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

        <span class="c1">// then</span>
        <span class="kt">var</span> <span class="n">updatedUser</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">updatedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="summary">Summary</h3>

<p>By combining In-Memory, Ability and a Base Class, our <code class="language-plaintext highlighter-rouge">BaseUnitTest</code>, we stop fighting the tools and start supporting the
process of delivering value. We managed to achieve three key goals:</p>

<ul>
  <li><strong>Isolation</strong>: Thanks to <code class="language-plaintext highlighter-rouge">@BeforeEach</code> in the base class, each test starts with an empty database. This eliminates
errors caused by data leaking between tests, which is a nightmare in large test suites.</li>
  <li><strong>One source of truth</strong>: <code class="language-plaintext highlighter-rouge">thereIsAUser</code> (Given), <code class="language-plaintext highlighter-rouge">userService.updateName</code> (When), and the assertion (Then) all operate
on the same <code class="language-plaintext highlighter-rouge">InMemoryUserRepository</code> instance. You do not have to configure anything manually — what you save in Given
is physically available in When and verifiable in Then.</li>
  <li><strong>Easier debugging</strong>: You will feel the biggest difference when a test… fails. In the Mockito world, you often end up
with an enigmatic “Wanted but not invoked” message. Here, instead of debugging the depths of the framework, you simply
put a breakpoint in the <code class="language-plaintext highlighter-rouge">updateName</code> method and step into it.</li>
</ul>

<h3 id="what-is-next">What is next?</h3>

<p>We now have unit tests that run fast and do not lie. In the next part, we will deal with integration tests.</p>

<p>You will learn:</p>

<ul>
  <li>How not to fall into the trap of unnecessarily reloading the Spring context and why the <code class="language-plaintext highlighter-rouge">@DirtiesContext</code> annotation is
probably your enemy rather than your friend.</li>
  <li>Why <code class="language-plaintext highlighter-rouge">@SpyBean</code> is an invitation to trouble and how to avoid it.</li>
  <li>How to make <code class="language-plaintext highlighter-rouge">Testcontainers</code> work so that integration tests are almost as pleasant and stable as today’s unit tests.</li>
</ul>]]></content><author><name></name></author><category term="java" /><category term="testing" /><category term="mockito" /><category term="software-architecture" /><summary type="html"><![CDATA[In the previous part, we focused on how to write tests that are simply pleasant to read. But readability is only half the battle. You can have the most beautifully written Given-When-Then section that… checks absolutely nothing. Today we will talk about trust in our tests. Because the worst kind of test is one that gives you a sense of safety, even though the code underneath does something completely different from what the test suggests. 1. The trap of testing implementation (White Box) I have noticed that in many projects Mockito is added to tests “automatically”. We generate a test class, mock all dependencies, and done. Few people ask themselves then: why am I actually using this mock? Imagine a simple service for updating user data. ☹️ Sad code: @Test void shouldUpdateUserName() { // given var userId = 1L; var user = new User(userId, "Jan"); // We have to "feed" the mock so the test can even start when(userRepository.findById(userId)).thenReturn(Optional.of(user)); // when userService.updateName(userId, "Jan Kowalski"); // then // We only check a technical method call. // Do we know whether the name was actually changed in the object before saving? // This test will say "YES" even if the service sends old data to save(). verify(userRepository).save(any(User.class)); } This test lies to you. It only checks whether the save method was called. If a developer mixes up fields and assigns the new value to a completely different field in production code, or skips the assignment entirely, this test will still be green. Instead of testing business behavior, which is changing the name, you test a technical library call. Okay, but someone may notice that we can still verify the object state and try to use ArgumentCaptor for update logic. ☹️ Even sadder code: @Test void shouldUpdateUserName_CaptorVersion() { // given var userId = 1L; var existingUser = new User(userId, "Jan"); var userCaptor = ArgumentCaptor.forClass(User.class); when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser)); // when userService.updateName(userId, "Jan Kowalski"); // then // This is where the trouble begins. We expose implementation details. verify(userRepository).save(userCaptor.capture()); var savedUser = userCaptor.getValue(); assertThat(savedUser.getName()).isEqualTo("Jan Kowalski"); } So, success? Not exactly. We have just entered White Box Testing mode. Tests become fragile (Fragile tests) because: Refactoring becomes painful: change save() to saveAll() and the test blows up, even though the business logic still works. You test “how”, not “what”: you care whether a specific line of code was called, not what the result is for the user. Sonar lies: reports show line coverage, but you did not test those lines — you only executed them in an artificial environment. Solution: In-Memory implementation Instead of fighting Mockito, let’s treat the service as a black box. We need something that pretends to be a database but works in memory. A ConcurrentHashMap under the repository is the simplest approach. public class InMemoryUserRepository implements UserRepository { private final Map&lt;Long, User&gt; db = new ConcurrentHashMap&lt;&gt;(); @Override public User save(User user) { db.put(user.getId(), user); return user; } @Override public Optional&lt;User&gt; findById(Long id) { return Optional.ofNullable(db.get(id)); } public void clear() { db.clear(); } } A state-based test could look like this. Now the test does not need any verify. We simply execute the action and check whether the state in the “database” is correct. class UserServiceTest { private final InMemoryUserRepository userRepository = new InMemoryUserRepository(); private final UserService userService = new UserService(userRepository); @BeforeEach void setup() { userRepository.clear(); } @Test void shouldUpdateUserName() { // given userRepository.save(new User(1L, "Jan")); // when userService.updateName(1L, "Jan Kowalski"); // then var updatedUser = userRepository.findById(1L).orElseThrow(); assertThat(updatedUser.getName()).isEqualTo("Jan Kowalski"); } } What about DSL? Remember the first part? We can use those patterns to prepare the initial state even more cleanly. Instead of manually calling userRepository.save() in the given section, we will use our “ability”. Happy code: @Test void shouldUpdateUserName() { // given thereIsAUser(anUser().withId(1L).withName("Jan").build()); // when userService.updateName(1L, "Jan Kowalski"); // then var updatedUser = userRepository.findById(1L).orElseThrow(); assertThat(updatedUser.getName()).isEqualTo("Jan Kowalski"); } Is this still White Box? Someone might say: “Wait, but in the assertion you call the repository!”. No. The difference is fundamental: In Mockito (Interaction): You ask, “Did you call the save method?”. If a developer changes the way the save works, the test fails. In-Memory (State): You ask, “System, no matter how you did it, does this user have a new name?”. In the Black Box approach, we treat the Service + InMemoryRepo pair as one black box. We do not care how many times the service “talked” to the repository. We care about the final effect. How it could look in the end You may wonder: where does Ability get the repository from and is it definitely the same instance that the service uses? This is the key point. For this to work, we need one source of truth. The best way is to use interfaces with default methods. public interface UserAbility { UserRepository userRepository(); // Provider method default void thereIsAUser(UserBuilder user) { userRepository().save(user); } } // builder in another package, for example com.ourdomain.testing.dsl.builders public class UserBuilder { private Long id = 1L; // Default ID private String name = "Jan"; // Default name // ... other fields public static UserBuilder anUser() { return new UserBuilder(); } public UserBuilder withId(Long id) { this.id = id; return this; } public UserBuilder withName(String name) { this.name = name; return this; } public User build() { return new User(id, name); } } Let’s create a base class for unit tests to hide technical implementation details of our DSL. public abstract class BaseUnitTest implements UserAbility { // One shared instance for the service, assertions, and all Abilities protected final InMemoryUserRepository userRepository = new InMemoryUserRepository(); @Override public UserRepository userRepository() { return userRepository; } @BeforeEach void clearDatabase() { userRepository.clear(); // Important for isolating tests: clean in-memory database state before each test } } Thanks to this, your test class simply extends BaseUnitTest and can use our In-Memory implementation. Here is what the final test class looks like. Notice how little technical noise is left. We focus only on business behavior. class UserServiceTest extends BaseUnitTest { // We inject the same repository that lives in BaseUnitTest private final UserService userService = new UserService(userRepository); @Test void shouldUpdateUserName() { // given thereIsAUser(anUser().withId(1L).withName("Jan")); // when userService.updateName(1L, "Jan Kowalski"); // then var updatedUser = userRepository.findById(1L).orElseThrow(); assertThat(updatedUser.getName()).isEqualTo("Jan Kowalski"); } } Summary By combining In-Memory, Ability and a Base Class, our BaseUnitTest, we stop fighting the tools and start supporting the process of delivering value. We managed to achieve three key goals: Isolation: Thanks to @BeforeEach in the base class, each test starts with an empty database. This eliminates errors caused by data leaking between tests, which is a nightmare in large test suites. One source of truth: thereIsAUser (Given), userService.updateName (When), and the assertion (Then) all operate on the same InMemoryUserRepository instance. You do not have to configure anything manually — what you save in Given is physically available in When and verifiable in Then. Easier debugging: You will feel the biggest difference when a test… fails. In the Mockito world, you often end up with an enigmatic “Wanted but not invoked” message. Here, instead of debugging the depths of the framework, you simply put a breakpoint in the updateName method and step into it. What is next? We now have unit tests that run fast and do not lie. In the next part, we will deal with integration tests. You will learn: How not to fall into the trap of unnecessarily reloading the Spring context and why the @DirtiesContext annotation is probably your enemy rather than your friend. Why @SpyBean is an invitation to trouble and how to avoid it. How to make Testcontainers work so that integration tests are almost as pleasant and stable as today’s unit tests.]]></summary></entry><entry xml:lang="pl"><title type="html">Testy, które nie kłamią cz. 2: Pułapka Mockito i implementacje In-Memory</title><link href="https://camilyed.github.io//pl/testy-ktore-nie-klamia-cz2/" rel="alternate" type="text/html" title="Testy, które nie kłamią cz. 2: Pułapka Mockito i implementacje In-Memory" /><published>2026-01-31T00:00:00+00:00</published><updated>2026-01-31T00:00:00+00:00</updated><id>https://camilyed.github.io//pl/testy-ktore-nie-klamia-cz2</id><content type="html" xml:base="https://camilyed.github.io//pl/testy-ktore-nie-klamia-cz2/"><![CDATA[<p>W poprzedniej części skupiliśmy się na tym, jak pisać testy, które po prostu dobrze się czyta. Ale czytelność to tylko
połowa sukcesu. Możesz mieć najpiękniej napisaną sekcję Given-When-Then, która… kompletnie nic nie sprawdza.</p>

<p>Dziś pogadamy o zaufaniu do naszych testów. Bo najgorszy rodzaj testu to taki, który daje Ci poczucie bezpieczeństwa,
mimo że Twój kod pod spodem robi zupełnie coś innego, na co by wskazywał sam test.</p>

<h3 id="1-pułapka-testowania-implementacji-white-box">1. Pułapka testowania implementacji (White Box)</h3>

<p>Zauważyłem, że w wielu projektach Mockito dodaje się do testów “z automatu”. Generujemy klasę testową, mockujemy
wszystkie zależności i cyk – robota zrobiona. Mało kto zadaje sobie wtedy pytanie: <strong>po co ja właściwie tego mocka
używam?</strong></p>

<p>Wyobraź sobie prosty serwis do aktualizacji danych użytkownika.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">userId</span> <span class="o">=</span> <span class="mi">1L</span><span class="o">;</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan"</span><span class="o">);</span>
    <span class="c1">// Musimy "nakarmić" mocka, żeby test w ogóle ruszył</span>
    <span class="n">when</span><span class="o">(</span><span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">)).</span><span class="na">thenReturn</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">user</span><span class="o">));</span>

    <span class="c1">// when</span>
    <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="c1">// Sprawdzamy tylko techniczne wywołanie metody. </span>
    <span class="c1">// Czy wiemy, czy imię faktycznie zostało zmienione w obiekcie przed zapisem? </span>
    <span class="c1">// Ten test powie "TAK", nawet jeśli serwis wyśle do save() stare dane.</span>
    <span class="n">verify</span><span class="o">(</span><span class="n">userRepository</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">any</span><span class="o">(</span><span class="nc">User</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Ten test Cię oszukuje. Sprawdza tylko, czy zawołano metodę save. Jeśli programista pomyli pola i w kodzie produkcyjnym
przypisze nową wartość do zupełnie innego pola (albo w ogóle pominie przypisanie), ten test nadal przejdzie na zielono!
Zamiast testować zachowanie biznesowe (zmiana imienia), testujesz techniczne wywołanie biblioteki.</p>

<p>No dobra, ale ktoś zauważy, że możemy jednak zweryfikować stan obiektu i spróbuje użyć <code class="language-plaintext highlighter-rouge">ArgumentCaptor</code> przy logice
aktualizacji danych (update).</p>

<p>☹️ Jeszcze bardziej smutny kodzik:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldUpdateUserName_CaptorVersion</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">userId</span> <span class="o">=</span> <span class="mi">1L</span><span class="o">;</span>
    <span class="kt">var</span> <span class="n">existingUser</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan"</span><span class="o">);</span>
    <span class="kt">var</span> <span class="n">userCaptor</span> <span class="o">=</span> <span class="nc">ArgumentCaptor</span><span class="o">.</span><span class="na">forClass</span><span class="o">(</span><span class="nc">User</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="n">when</span><span class="o">(</span><span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">userId</span><span class="o">)).</span><span class="na">thenReturn</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">existingUser</span><span class="o">));</span>

    <span class="c1">// when</span>
    <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="c1">// Tutaj zaczynają się schody. Odsłaniamy szczegóły implementacji.</span>
    <span class="n">verify</span><span class="o">(</span><span class="n">userRepository</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">userCaptor</span><span class="o">.</span><span class="na">capture</span><span class="o">());</span>
    <span class="kt">var</span> <span class="n">savedUser</span> <span class="o">=</span> <span class="n">userCaptor</span><span class="o">.</span><span class="na">getValue</span><span class="o">();</span>

    <span class="n">assertThat</span><span class="o">(</span><span class="n">savedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>I co? Sukces? No nie do końca. Właśnie weszliśmy w tryb <code class="language-plaintext highlighter-rouge">White Box Testing.</code> Testy stają się kruche (<code class="language-plaintext highlighter-rouge">Fragile tests</code>),
bo:</p>

<ul>
  <li>Refaktoryzacja to ból: Zmieniasz <code class="language-plaintext highlighter-rouge">save()</code> na <code class="language-plaintext highlighter-rouge">saveAll()</code>? Test wybucha, mimo że logika biznesowa działa.</li>
  <li>Testujesz “jak”, a nie “co”: Obchodzi Cię, czy wywołałeś konkretną linię kodu, a nie jaki jest wynik dla użytkownika.</li>
  <li>Sonar kłamie: Raporty pokazują pokrycie linii, ale Ty ich nie przetestowałeś – Ty je tylko wywołałeś w sztucznym
środowisku.</li>
</ul>

<h4 id="rozwiązanie-implementacja-in-memory">Rozwiązanie: Implementacja In-Memory</h4>

<p>Zamiast walczyć z Mockito, potraktujmy serwis jako czarną skrzynkę. Potrzebujemy czegoś, co udaje bazę danych, ale
działa w pamięci. <code class="language-plaintext highlighter-rouge">ConcurrentHashMap</code> pod spodem repozytorium to najprostszy sposób.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">InMemoryUserRepository</span> <span class="kd">implements</span> <span class="nc">UserRepository</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">,</span> <span class="nc">User</span><span class="o">&gt;</span> <span class="n">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">save</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">db</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">user</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">ofNullable</span><span class="o">(</span><span class="n">db</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">id</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">clear</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">db</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Tak mógłby wyglądać wtedy test oparty na stanie <code class="language-plaintext highlighter-rouge">State-based</code>.
Teraz nasz test nie potrzebuje żadnych <code class="language-plaintext highlighter-rouge">verify</code>. Po prostu wywołujemy akcję i sprawdzamy, czy stan w “bazie” się zgadza.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserServiceTest</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">InMemoryUserRepository</span> <span class="n">userRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryUserRepository</span><span class="o">();</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">UserService</span> <span class="n">userService</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UserService</span><span class="o">(</span><span class="n">userRepository</span><span class="o">);</span>

    <span class="nd">@BeforeEach</span>
    <span class="kt">void</span> <span class="nf">setup</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// given</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan"</span><span class="o">));</span>

        <span class="c1">// when</span>
        <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

        <span class="c1">// then</span>
        <span class="kt">var</span> <span class="n">updatedUser</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">updatedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="a-co-z-dsl">A co z DSL?</h4>

<p>Pamiętasz pierwszą część? Możemy użyć tamtych wzorców, aby przygotować stan początkowy jeszcze czyściej. Zamiast ręcznie
wywoływać userRepository.save() w sekcji given, użyjemy naszej “zdolności” (Ability).</p>

<p>🙂 Uśmiechnięty kodzik:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="n">thereIsAUser</span><span class="o">(</span><span class="n">anUser</span><span class="o">().</span><span class="na">withId</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">withName</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">).</span><span class="na">build</span><span class="o">());</span>

    <span class="c1">// when</span>
    <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="kt">var</span> <span class="n">updatedUser</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updatedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Czy to nadal White Box? Ktoś powie: “Zaraz, ale w asercji wywołujesz repozytorium!”. Nie. Różnica jest fundamentalna:</p>

<ul>
  <li>W Mockito (Interakcja): Pytasz: “Czy zawołałeś metodę save?”. Jeśli programista zmieni sposób zapisu, test padnie.</li>
  <li>W In-Memory (Stan): Pytasz: “Systemie, nieważne jak to zrobiłeś, czy ten użytkownik ma nowe imię?”.</li>
</ul>

<p>W podejściu <code class="language-plaintext highlighter-rouge">Black Box</code> traktujemy parę Service + InMemoryRepo jako jedną czarną skrzynkę. Nie obchodzi nas, ile razy
serwis “gadał” z repozytorium. Obchodzi nas efekt końcowy.</p>

<h4 id="jak-to-mogło-by-wyglądać-ostatecznie">Jak to mogło by wyglądać ostatecznie</h4>

<p>Możesz się zastanawiać: Skąd <code class="language-plaintext highlighter-rouge">Ability</code> bierze repozytorium i czy to na pewno ta sama instancja, której używa serwis? To
kluczowy punkt. Żeby to działało, musimy mieć jedno źródło prawdy.</p>

<p>Najlepszym sposobem jest użycie interfejsów z domyślnymi implementacjami (default methods).</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserAbility</span> <span class="o">{</span>
    <span class="nc">UserRepository</span> <span class="nf">userRepository</span><span class="o">();</span> <span class="c1">// Metoda "dostawca"</span>

    <span class="k">default</span> <span class="kt">void</span> <span class="nf">thereIsAUser</span><span class="o">(</span><span class="nc">UserBuilder</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">userRepository</span><span class="o">().</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// builder w innym pakiece np. com.ourdomain.testing.dsl.builders</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserBuilder</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">1L</span><span class="o">;</span> <span class="c1">// Domyślne ID</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"Jan"</span><span class="o">;</span> <span class="c1">// Domyślne imię</span>
    <span class="c1">// ... inne pola</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">UserBuilder</span> <span class="nf">anUser</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">UserBuilder</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withId</span><span class="o">(</span><span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">id</span> <span class="o">=</span> <span class="n">id</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">build</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">User</span><span class="o">(</span><span class="n">id</span><span class="o">,</span> <span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Stwórzmy sobie klasę bazową dla testów unitowych, aby ukryć detale technicznej implementacji naszego DSL.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">BaseUnitTest</span> <span class="kd">implements</span> <span class="nc">UserAbility</span> <span class="o">{</span>
    
    <span class="c1">// Jedna, wspólna instancja dla serwisu, asercji i wszystkich Ability</span>
    <span class="kd">protected</span> <span class="kd">final</span> <span class="nc">InMemoryUserRepository</span> <span class="n">userRepository</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InMemoryUserRepository</span><span class="o">();</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">UserRepository</span> <span class="nf">userRepository</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">userRepository</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@BeforeEach</span>
    <span class="kt">void</span> <span class="nf">clearDatabase</span><span class="o">()</span> <span class="o">{</span>
        <span class="n">userRepository</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span> <span class="c1">// Ważne aby izolować testy czyścimy stan bazy w pamięci przed każdym</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Dzięki temu Twoja klasa testowa po prostu dziedziczy po <code class="language-plaintext highlighter-rouge">BaseUnitTest</code> i ma już możliwość skorzystania z naszej
implementacji <code class="language-plaintext highlighter-rouge">In-Memory</code>.Oto jak wygląda ostateczny kod Twojej klasy testowej. Zauważ, jak mało “szumu technicznego” tu
zostało. Skupiamy się wyłącznie na zachowaniu biznesowym.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserServiceTest</span> <span class="kd">extends</span> <span class="nc">BaseUnitTest</span> <span class="o">{</span>

    <span class="c1">// Wstrzykujemy to samo repozytorium, które siedzi w BaseUnitTest</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">UserService</span> <span class="n">userService</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UserService</span><span class="o">(</span><span class="n">userRepository</span><span class="o">);</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">shouldUpdateUserName</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// given </span>
        <span class="n">thereIsAUser</span><span class="o">(</span><span class="n">anUser</span><span class="o">().</span><span class="na">withId</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">withName</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">));</span>

        <span class="c1">// when</span>
        <span class="n">userService</span><span class="o">.</span><span class="na">updateName</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Jan Kowalski"</span><span class="o">);</span>

        <span class="c1">// then</span>
        <span class="kt">var</span> <span class="n">updatedUser</span> <span class="o">=</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="mi">1L</span><span class="o">).</span><span class="na">orElseThrow</span><span class="o">();</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">updatedUser</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="podsumowanie">Podsumowanie</h3>

<p>Stosując połączenie In-Memory, Ability oraz Base Class (nasza klasa <code class="language-plaintext highlighter-rouge">BaseUnitTest</code>), przestajemy walczyć z narzędziami,
a zaczynamy wspierać proces dostarczania wartości. Udało nam się osiągnąć trzy kluczowe cele:</p>

<ul>
  <li><strong>Izolacja</strong>: Dzięki <code class="language-plaintext highlighter-rouge">@BeforeEach</code> w klasie bazowej każdy test startuje z pustą bazą. Eliminuje
to błędy wynikające z wyciekania danych między testami, co jest zmorą dużych zestawów testowych.</li>
  <li><strong>Jedno źródło prawdy</strong>: Zarówno <code class="language-plaintext highlighter-rouge">thereIsAUser</code> (Given), <code class="language-plaintext highlighter-rouge">userService.updateName</code> (When), jak i asercja (Then) operują
na tej samej instancji <code class="language-plaintext highlighter-rouge">InMemoryUserRepository</code>. Nie musisz niczego konfigurować ręcznie – to, co zapiszesz w Given,
jest fizycznie dostępne w When i weryfikowalne w Then.</li>
  <li><strong>Łatwiejszy debug</strong>: Największą różnicę odczujesz, gdy test… nie przejdzie. W świecie Mockito często kończysz z
enigmatycznym komunikatem Wanted but not invoked. Tutaj, zamiast debugować czeluści frameworka, po prostu stawiasz
breakpoint w metodzie <code class="language-plaintext highlighter-rouge">updateName</code> i robisz Step Into.</li>
</ul>

<h3 id="co-dalej">Co dalej?</h3>

<p>Mamy już unity, które działają błyskawicznie i nie kłamią. W kolejnej części zajmiemy się testami integracyjnymi.</p>

<p>Dowiesz się:</p>

<ul>
  <li>Jak nie wpaść w pułapkę przeładowywania niepotrzebnie kontekstu Springa i dlaczego adnotacja <code class="language-plaintext highlighter-rouge">@DirtiesContext</code> to
jednak raczej Twój wróg niż przyjaciel.</li>
  <li>Dlaczego <code class="language-plaintext highlighter-rouge">@SpyBean</code> to zaproszenie do kłopotów i jak go unikać.</li>
  <li>Jak zaprząc <code class="language-plaintext highlighter-rouge">Testcontainers</code> do pracy tak, aby testy integracyjne były niemal tak przyjemne i stabilne, jak nasze
dzisiejsze unity.</li>
</ul>]]></content><author><name></name></author><category term="java" /><category term="testing" /><category term="mockito" /><category term="software-architecture" /><summary type="html"><![CDATA[W poprzedniej części skupiliśmy się na tym, jak pisać testy, które po prostu dobrze się czyta. Ale czytelność to tylko połowa sukcesu. Możesz mieć najpiękniej napisaną sekcję Given-When-Then, która… kompletnie nic nie sprawdza. Dziś pogadamy o zaufaniu do naszych testów. Bo najgorszy rodzaj testu to taki, który daje Ci poczucie bezpieczeństwa, mimo że Twój kod pod spodem robi zupełnie coś innego, na co by wskazywał sam test. 1. Pułapka testowania implementacji (White Box) Zauważyłem, że w wielu projektach Mockito dodaje się do testów “z automatu”. Generujemy klasę testową, mockujemy wszystkie zależności i cyk – robota zrobiona. Mało kto zadaje sobie wtedy pytanie: po co ja właściwie tego mocka używam? Wyobraź sobie prosty serwis do aktualizacji danych użytkownika. ☹️ Smutny kodzik: @Test void shouldUpdateUserName() { // given var userId = 1L; var user = new User(userId, "Jan"); // Musimy "nakarmić" mocka, żeby test w ogóle ruszył when(userRepository.findById(userId)).thenReturn(Optional.of(user)); // when userService.updateName(userId, "Jan Kowalski"); // then // Sprawdzamy tylko techniczne wywołanie metody. // Czy wiemy, czy imię faktycznie zostało zmienione w obiekcie przed zapisem? // Ten test powie "TAK", nawet jeśli serwis wyśle do save() stare dane. verify(userRepository).save(any(User.class)); } Ten test Cię oszukuje. Sprawdza tylko, czy zawołano metodę save. Jeśli programista pomyli pola i w kodzie produkcyjnym przypisze nową wartość do zupełnie innego pola (albo w ogóle pominie przypisanie), ten test nadal przejdzie na zielono! Zamiast testować zachowanie biznesowe (zmiana imienia), testujesz techniczne wywołanie biblioteki. No dobra, ale ktoś zauważy, że możemy jednak zweryfikować stan obiektu i spróbuje użyć ArgumentCaptor przy logice aktualizacji danych (update). ☹️ Jeszcze bardziej smutny kodzik: @Test void shouldUpdateUserName_CaptorVersion() { // given var userId = 1L; var existingUser = new User(userId, "Jan"); var userCaptor = ArgumentCaptor.forClass(User.class); when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser)); // when userService.updateName(userId, "Jan Kowalski"); // then // Tutaj zaczynają się schody. Odsłaniamy szczegóły implementacji. verify(userRepository).save(userCaptor.capture()); var savedUser = userCaptor.getValue(); assertThat(savedUser.getName()).isEqualTo("Jan Kowalski"); } I co? Sukces? No nie do końca. Właśnie weszliśmy w tryb White Box Testing. Testy stają się kruche (Fragile tests), bo: Refaktoryzacja to ból: Zmieniasz save() na saveAll()? Test wybucha, mimo że logika biznesowa działa. Testujesz “jak”, a nie “co”: Obchodzi Cię, czy wywołałeś konkretną linię kodu, a nie jaki jest wynik dla użytkownika. Sonar kłamie: Raporty pokazują pokrycie linii, ale Ty ich nie przetestowałeś – Ty je tylko wywołałeś w sztucznym środowisku. Rozwiązanie: Implementacja In-Memory Zamiast walczyć z Mockito, potraktujmy serwis jako czarną skrzynkę. Potrzebujemy czegoś, co udaje bazę danych, ale działa w pamięci. ConcurrentHashMap pod spodem repozytorium to najprostszy sposób. public class InMemoryUserRepository implements UserRepository { private final Map&lt;Long, User&gt; db = new ConcurrentHashMap&lt;&gt;(); @Override public User save(User user) { db.put(user.getId(), user); return user; } @Override public Optional&lt;User&gt; findById(Long id) { return Optional.ofNullable(db.get(id)); } public void clear() { db.clear(); } } Tak mógłby wyglądać wtedy test oparty na stanie State-based. Teraz nasz test nie potrzebuje żadnych verify. Po prostu wywołujemy akcję i sprawdzamy, czy stan w “bazie” się zgadza. class UserServiceTest { private final InMemoryUserRepository userRepository = new InMemoryUserRepository(); private final UserService userService = new UserService(userRepository); @BeforeEach void setup() { userRepository.clear(); } @Test void shouldUpdateUserName() { // given userRepository.save(new User(1L, "Jan")); // when userService.updateName(1L, "Jan Kowalski"); // then var updatedUser = userRepository.findById(1L).orElseThrow(); assertThat(updatedUser.getName()).isEqualTo("Jan Kowalski"); } } A co z DSL? Pamiętasz pierwszą część? Możemy użyć tamtych wzorców, aby przygotować stan początkowy jeszcze czyściej. Zamiast ręcznie wywoływać userRepository.save() w sekcji given, użyjemy naszej “zdolności” (Ability). 🙂 Uśmiechnięty kodzik: @Test void shouldUpdateUserName() { // given thereIsAUser(anUser().withId(1L).withName("Jan").build()); // when userService.updateName(1L, "Jan Kowalski"); // then var updatedUser = userRepository.findById(1L).orElseThrow(); assertThat(updatedUser.getName()).isEqualTo("Jan Kowalski"); } Czy to nadal White Box? Ktoś powie: “Zaraz, ale w asercji wywołujesz repozytorium!”. Nie. Różnica jest fundamentalna: W Mockito (Interakcja): Pytasz: “Czy zawołałeś metodę save?”. Jeśli programista zmieni sposób zapisu, test padnie. W In-Memory (Stan): Pytasz: “Systemie, nieważne jak to zrobiłeś, czy ten użytkownik ma nowe imię?”. W podejściu Black Box traktujemy parę Service + InMemoryRepo jako jedną czarną skrzynkę. Nie obchodzi nas, ile razy serwis “gadał” z repozytorium. Obchodzi nas efekt końcowy. Jak to mogło by wyglądać ostatecznie Możesz się zastanawiać: Skąd Ability bierze repozytorium i czy to na pewno ta sama instancja, której używa serwis? To kluczowy punkt. Żeby to działało, musimy mieć jedno źródło prawdy. Najlepszym sposobem jest użycie interfejsów z domyślnymi implementacjami (default methods). public interface UserAbility { UserRepository userRepository(); // Metoda "dostawca" default void thereIsAUser(UserBuilder user) { userRepository().save(user); } } // builder w innym pakiece np. com.ourdomain.testing.dsl.builders public class UserBuilder { private Long id = 1L; // Domyślne ID private String name = "Jan"; // Domyślne imię // ... inne pola public static UserBuilder anUser() { return new UserBuilder(); } public UserBuilder withId(Long id) { this.id = id; return this; } public UserBuilder withName(String name) { this.name = name; return this; } public User build() { return new User(id, name); } } Stwórzmy sobie klasę bazową dla testów unitowych, aby ukryć detale technicznej implementacji naszego DSL. public abstract class BaseUnitTest implements UserAbility { // Jedna, wspólna instancja dla serwisu, asercji i wszystkich Ability protected final InMemoryUserRepository userRepository = new InMemoryUserRepository(); @Override public UserRepository userRepository() { return userRepository; } @BeforeEach void clearDatabase() { userRepository.clear(); // Ważne aby izolować testy czyścimy stan bazy w pamięci przed każdym } } Dzięki temu Twoja klasa testowa po prostu dziedziczy po BaseUnitTest i ma już możliwość skorzystania z naszej implementacji In-Memory.Oto jak wygląda ostateczny kod Twojej klasy testowej. Zauważ, jak mało “szumu technicznego” tu zostało. Skupiamy się wyłącznie na zachowaniu biznesowym. class UserServiceTest extends BaseUnitTest { // Wstrzykujemy to samo repozytorium, które siedzi w BaseUnitTest private final UserService userService = new UserService(userRepository); @Test void shouldUpdateUserName() { // given thereIsAUser(anUser().withId(1L).withName("Jan")); // when userService.updateName(1L, "Jan Kowalski"); // then var updatedUser = userRepository.findById(1L).orElseThrow(); assertThat(updatedUser.getName()).isEqualTo("Jan Kowalski"); } } Podsumowanie Stosując połączenie In-Memory, Ability oraz Base Class (nasza klasa BaseUnitTest), przestajemy walczyć z narzędziami, a zaczynamy wspierać proces dostarczania wartości. Udało nam się osiągnąć trzy kluczowe cele: Izolacja: Dzięki @BeforeEach w klasie bazowej każdy test startuje z pustą bazą. Eliminuje to błędy wynikające z wyciekania danych między testami, co jest zmorą dużych zestawów testowych. Jedno źródło prawdy: Zarówno thereIsAUser (Given), userService.updateName (When), jak i asercja (Then) operują na tej samej instancji InMemoryUserRepository. Nie musisz niczego konfigurować ręcznie – to, co zapiszesz w Given, jest fizycznie dostępne w When i weryfikowalne w Then. Łatwiejszy debug: Największą różnicę odczujesz, gdy test… nie przejdzie. W świecie Mockito często kończysz z enigmatycznym komunikatem Wanted but not invoked. Tutaj, zamiast debugować czeluści frameworka, po prostu stawiasz breakpoint w metodzie updateName i robisz Step Into. Co dalej? Mamy już unity, które działają błyskawicznie i nie kłamią. W kolejnej części zajmiemy się testami integracyjnymi. Dowiesz się: Jak nie wpaść w pułapkę przeładowywania niepotrzebnie kontekstu Springa i dlaczego adnotacja @DirtiesContext to jednak raczej Twój wróg niż przyjaciel. Dlaczego @SpyBean to zaproszenie do kłopotów i jak go unikać. Jak zaprząc Testcontainers do pracy tak, aby testy integracyjne były niemal tak przyjemne i stabilne, jak nasze dzisiejsze unity.]]></summary></entry><entry xml:lang="en"><title type="html">Tests That Don’t Lie, Part 1: Readability and DSL</title><link href="https://camilyed.github.io//en/tests-that-dont-lie/" rel="alternate" type="text/html" title="Tests That Don’t Lie, Part 1: Readability and DSL" /><published>2026-01-25T00:00:00+00:00</published><updated>2026-01-25T00:00:00+00:00</updated><id>https://camilyed.github.io//en/tests-that-dont-lie-readability-and-dsl</id><content type="html" xml:base="https://camilyed.github.io//en/tests-that-dont-lie/"><![CDATA[<p>In this article, I want to share how I approach writing tests that are not there only to increase so-called
code coverage, but give me confidence that after deploying a new feature, it works according to the original
assumptions.</p>

<h2 id="readability">Readability</h2>

<p>Let’s start with the basics. If a test does not clearly communicate what is being tested and why it failed, then all the
technology around it becomes unnecessary weight. Many times, while reviewing tests during code review, I really have to
make an effort to understand what they are actually testing, without trusting the test name too much.</p>

<!--more-->

<h3 id="1-given-when-then-structure">1. Given-When-Then structure</h3>

<p>Let’s look at the first example. It is not complex at all, but already at the start it makes us work harder to extract
what is the test input, what behavior we are testing, and what assertion we check at the end.</p>

<p>☹️ <strong>Sad code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">updateTest</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">changeStatus</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="nc">User</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertEquals</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">,</span> <span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Simple code separators, for example comments, make it much easier at first glance to understand where we create the
input setup — <code class="language-plaintext highlighter-rouge">// given</code>, what we test — <code class="language-plaintext highlighter-rouge">// when</code>, and what we verify — <code class="language-plaintext highlighter-rouge">// then</code>.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldChangeUserStatusToInactive</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="n">user</span><span class="o">.</span><span class="na">changeStatus</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="kt">var</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>As our tests become more realistic, the data preparation and assertion sections can grow. Instead of creating one huge
block of code under <code class="language-plaintext highlighter-rouge">// given</code>, it is worth using the helper word <code class="language-plaintext highlighter-rouge">// and</code>. It lets us logically group operations, for
example separate creating a user from setting permissions or preparing database state.</p>

<p>Let’s look at a slightly more complex example:</p>

<p>☹️ <strong>Sad code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">complexUpdateTest</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setAddress</span><span class="o">(</span><span class="k">new</span> <span class="nc">Address</span><span class="o">(</span><span class="s">"Warszawa"</span><span class="o">,</span> <span class="s">"Złota 44"</span><span class="o">));</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="nc">AuditLog</span> <span class="n">log</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">auditRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">log</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">changeStatus</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setDeactivationReason</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="nc">User</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertEquals</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">,</span> <span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">());</span>
    <span class="n">assertEquals</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">,</span> <span class="n">updated</span><span class="o">.</span><span class="na">getDeactivationReason</span><span class="o">());</span>
    <span class="n">assertNotNull</span><span class="o">(</span><span class="n">auditRepository</span><span class="o">.</span><span class="na">findByUserIdAndType</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="s">"STATUS_CHANGE"</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Even in such a small test, we start squinting to understand what is background and what is the actual action. Using the
<code class="language-plaintext highlighter-rouge">and</code> structure significantly improves readability:</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="c1">// and</span>
    <span class="kt">var</span> <span class="n">initialLog</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">auditRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">initialLog</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="n">user</span><span class="o">.</span><span class="na">deactivate</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="kt">var</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getDeactivationReason</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="c1">// and</span>
    <span class="kt">var</span> <span class="n">statusLog</span> <span class="o">=</span> <span class="n">auditRepository</span><span class="o">.</span><span class="na">findByUserIdAndType</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="s">"STATUS_CHANGE"</span><span class="o">);</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">statusLog</span><span class="o">).</span><span class="na">isNotNull</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Is that it?</strong></p>

<p>Even though the code above looks much better than a wall of text, there is still a lot of technical noise here: manually
setting fields, setters, and technical assertions one after another.</p>

<p>In the next sections, we will see how patterns such as Test Data Builder and Custom Assertions can make the same test
look almost like sentences in natural language.</p>

<hr />

<h3 id="2-test-data-builder">2. Test Data Builder</h3>

<p>Let’s return to the “sad code” from the section above. Why does it actually hurt to look at it? Because every time we
want to create a user, we have to call a constructor with all fields or a set of setters. This creates a lot of
information noise. Not all data set in the constructor has any impact on the assertion result. What is more, if the
constructor is extended with one more argument, our tests require changes everywhere that constructor is called. This is
when we deal with <code class="language-plaintext highlighter-rouge">fragile tests</code>.</p>

<p>The solution to this situation is the <strong>Test Data Builder</strong> pattern described
by <a href="http://www.natpryce.com/articles/000714.html">Nat Pryce</a>. The idea is to create a helper class that has reasonable
default values for all fields. In the test itself, we override only the parameters that are important for a given
scenario.</p>

<p>Before we move to the Builder implementation, let’s see what the <code class="language-plaintext highlighter-rouge">// given</code> section looks like when our domain becomes
richer. Let’s assume that in order to save a user in the database, we must satisfy several technical requirements:
address, contact details, dates, permissions.</p>

<p>In a deactivation test, these details are only background, but in the code they take the foreground:</p>

<p>☹️ <strong>Sad code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">address</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Address</span><span class="o">(</span><span class="s">"Warszawa"</span><span class="o">,</span> <span class="s">"Złota 44"</span><span class="o">,</span> <span class="s">"00-123"</span><span class="o">,</span> <span class="s">"Polska"</span><span class="o">);</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Kowalski"</span><span class="o">,</span> <span class="s">"jan.k@example.com"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setAddress</span><span class="o">(</span><span class="n">address</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setCreatedAt</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">());</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setLastLogin</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">().</span><span class="na">minusDays</span><span class="o">(</span><span class="mi">1</span><span class="o">));</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// and - preparing technical logs that must exist in the system</span>
    <span class="kt">var</span> <span class="n">initialLog</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">(),</span> <span class="s">"SYSTEM"</span><span class="o">);</span>
    <span class="n">auditRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">initialLog</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="n">user</span><span class="o">.</span><span class="na">deactivate</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// then - we only verify the status field</span>
    <span class="kt">var</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>We have as many as 8 lines of code only to prepare the object for the test. Does any of these parameters affect whether
the administrator is correctly deactivated? Of course not. Since these technical details are irrelevant from the point of
view of the deactivation business logic, they should be hidden.</p>

<p>This is exactly where <code class="language-plaintext highlighter-rouge">Test Data Builder</code> helps. It allows us to define reasonable default values for all required fields
in one place. What is more, we can go one step further and compose builders. If our <code class="language-plaintext highlighter-rouge">User</code> has an <code class="language-plaintext highlighter-rouge">Address</code>, and the
address does not matter in a given test, the user builder simply uses the default address builder.</p>

<p>Let’s look at the implementation:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserBuilder</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"Jan"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">lastName</span> <span class="o">=</span> <span class="s">"Kowalski"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">status</span> <span class="o">=</span> <span class="s">"Inactive"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">role</span> <span class="o">=</span> <span class="s">"USER"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">deactivationReason</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">AddressBuilder</span> <span class="n">addressBuilder</span> <span class="o">=</span> <span class="nc">AddressBuilder</span><span class="o">.</span><span class="na">anAddress</span><span class="o">();</span>

    <span class="kd">private</span> <span class="nf">UserBuilder</span><span class="o">()</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">UserBuilder</span> <span class="nf">aUser</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">UserBuilder</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withRole</span><span class="o">(</span><span class="nc">String</span> <span class="n">role</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">role</span> <span class="o">=</span> <span class="n">role</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withStatus</span><span class="o">(</span><span class="nc">String</span> <span class="n">status</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">status</span> <span class="o">=</span> <span class="n">status</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withAddress</span><span class="o">(</span><span class="nc">AddressBuilder</span> <span class="n">addressBuilder</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">addressBuilder</span> <span class="o">=</span> <span class="n">addressBuilder</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">build</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">lastName</span><span class="o">,</span> <span class="n">status</span><span class="o">,</span> <span class="n">role</span><span class="o">);</span>
        <span class="n">user</span><span class="o">.</span><span class="na">setAddress</span><span class="o">(</span><span class="n">addressBuilder</span><span class="o">.</span><span class="na">build</span><span class="o">());</span>
        <span class="n">user</span><span class="o">.</span><span class="na">setDeactivationReason</span><span class="o">(</span><span class="n">deactivationReason</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AddressBuilder</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">city</span> <span class="o">=</span> <span class="s">"Warszawa"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">street</span> <span class="o">=</span> <span class="s">"Złota 44"</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">AddressBuilder</span> <span class="nf">anAddress</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">AddressBuilder</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">AddressBuilder</span> <span class="nf">withCity</span><span class="o">(</span><span class="nc">String</span> <span class="n">city</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">city</span> <span class="o">=</span> <span class="n">city</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">Address</span> <span class="nf">build</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">Address</span><span class="o">(</span><span class="n">city</span><span class="o">,</span> <span class="n">street</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>What do we gain?</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">aUser</span><span class="o">()</span>
            <span class="o">.</span><span class="na">withRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>

    <span class="c1">// rest of the code...</span>
<span class="o">}</span>
</code></pre></div></div>

<p>As you can see, we have hidden all irrelevant information noise in the given section, where we focus only on the user’s
role, because that is what matters in this test.</p>

<hr />

<h3 id="3-assertions---the-most-common-mistakes">3. Assertions - the most common mistakes</h3>

<p>Now that we have perfectly prepared input data, we must make sure that the test result actually tells us something.
There are two popular practices that give a false sense of safety.</p>

<h4 id="31-shared-variables">3.1 Shared variables</h4>

<p>It is very tempting to define a value once, for example a user’s name, and use it both in the <code class="language-plaintext highlighter-rouge">// given</code> and <code class="language-plaintext highlighter-rouge">// then</code>
sections. This is a mistake. If you accidentally change the value at the beginning of the test, the assertion at the end
will still be green, even though the system may behave incorrectly.</p>

<p>☹️ <strong>Sad code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">expectedName</span> <span class="o">=</span> <span class="s">"Jan"</span><span class="o">;</span> <span class="c1">// If you change this to "Anna"...</span>
<span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">aUser</span><span class="o">().</span><span class="na">withName</span><span class="o">(</span><span class="n">expectedName</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>

<span class="c1">// rest of the test ...</span>

<span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">expectedName</span><span class="o">);</span> 
<span class="c1">// ...the test still passes!</span>
</code></pre></div></div>

<p>To make the code above resistant to this kind of situation, it is enough to use string literals.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">aUser</span><span class="o">().</span><span class="na">withName</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>

<span class="c1">// rest of the test ...</span>

<span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">);</span>
</code></pre></div></div>

<h4 id="32-dto-classes-in-assertions">3.2 DTO classes in assertions</h4>

<p>In API tests, it is common to see a practice where the response from an endpoint is mapped to a DTO class representing
the JSON response.</p>

<p>☹️ <strong>Sad code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldGetUserDetails</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// when</span>
    <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">UserResponse</span><span class="o">&gt;</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">getForEntity</span><span class="o">(</span><span class="s">"/users/1"</span><span class="o">,</span> <span class="nc">UserResponse</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getStatusCode</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">OK</span><span class="o">);</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">().</span><span class="na">getFullName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Why do I consider this an unsafe practice? If you rename a field in the <code class="language-plaintext highlighter-rouge">UserResponse</code> class, for example from <code class="language-plaintext highlighter-rouge">fullName</code>
to <code class="language-plaintext highlighter-rouge">name</code>, the IDE refactoring will automatically update this name in the test as well. The result is that the test still
passes, but the endpoint contract is already broken. This is a common example of a <code class="language-plaintext highlighter-rouge">False Positive</code> test.</p>

<p>In integration tests, it is better to check the raw response, for example as a String or a map, or use a library such as
JsonPath, which looks directly into the JSON structure.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldGetUserDetailsAndValidateContract</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// omitted code, test setup that puts the user in the database ...</span>

    <span class="c1">// when</span>
    <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">getForEntity</span><span class="o">(</span><span class="s">"/users/1"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="nc">JsonPath</span><span class="o">.</span><span class="na">read</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">(),</span> <span class="s">"$.fullName"</span><span class="o">)).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h3 id="4-custom-assertion">4. Custom Assertion</h3>

<p>In the <code class="language-plaintext highlighter-rouge">// then</code> section, I often see code that tries to verify the system state after a complex process. Instead of a
clear signal, we get logic, loops, and manual data extraction.</p>

<p>☹️ <strong>Sad code:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// then</span>
<span class="kt">var</span> <span class="n">logs</span> <span class="o">=</span> <span class="n">auditRepository</span><span class="o">.</span><span class="na">findByUserId</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>

<span class="c1">// We must check whether anything came back at all</span>
<span class="n">assertNotNull</span><span class="o">(</span><span class="n">logs</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="mi">3</span><span class="o">,</span> <span class="n">logs</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>

<span class="c1">// We look for a specific deactivation log among many others</span>
<span class="nc">AuditLog</span> <span class="n">deactivationLog</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">AuditLog</span> <span class="n">log</span> <span class="o">:</span> <span class="n">logs</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="s">"USER_DEACTIVATED"</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">log</span><span class="o">.</span><span class="na">getEventName</span><span class="o">()))</span> <span class="o">{</span>
        <span class="n">deactivationLog</span> <span class="o">=</span> <span class="n">log</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// We check details - a lot of technical assertions</span>
<span class="n">assertNotNull</span><span class="o">(</span><span class="n">deactivationLog</span><span class="o">,</span> <span class="s">"The deactivation log should exist!"</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"PROCESSED"</span><span class="o">,</span> <span class="n">deactivationLog</span><span class="o">.</span><span class="na">getStatus</span><span class="o">());</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"AUTH_SERVICE"</span><span class="o">,</span> <span class="n">deactivationLog</span><span class="o">.</span><span class="na">getSystemName</span><span class="o">());</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"ADMIN_123"</span><span class="o">,</span> <span class="n">deactivationLog</span><span class="o">.</span><span class="na">getActorId</span><span class="o">());</span>
<span class="n">assertTrue</span><span class="o">(</span><span class="n">deactivationLog</span><span class="o">.</span><span class="na">getTimestamp</span><span class="o">().</span><span class="na">isAfter</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">().</span><span class="na">minusMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">)));</span>

<span class="c1">// And we also check the state of the remaining logs</span>
<span class="n">logs</span><span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="n">l</span> <span class="o">-&gt;</span> <span class="n">assertEquals</span><span class="o">(</span><span class="s">"SUCCESS"</span><span class="o">,</span> <span class="n">l</span><span class="o">.</span><span class="na">getDeliveryStatus</span><span class="o">()));</span>
</code></pre></div></div>

<p>Why do I consider this approach bad?</p>

<ol>
  <li>A loop in a test: If you have a for or if in a test, you are effectively writing an algorithm. And algorithms can be
wrong. Do we now need a test for the test?</li>
  <li>Low-level details: When reading this, you have to analyze how the <code class="language-plaintext highlighter-rouge">for</code> works, how we compare Strings, and whether
<code class="language-plaintext highlighter-rouge">assertNotNull</code> is in the right place. The business intention (“the user was deactivated and this was recorded in the
audit log”) disappears in a thicket of technical instructions.</li>
  <li>Domino effect: If the log structure changes, you have to fix these 15 lines of code in every test that checks it.</li>
</ol>

<p>Solution: A custom assertion class, internally using for example the <code class="language-plaintext highlighter-rouge">AssertJ</code> library:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserAssert</span> <span class="kd">extends</span> <span class="nc">AbstractAssert</span><span class="o">&lt;</span><span class="nc">UserAssert</span><span class="o">,</span> <span class="nc">User</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nc">UserAssert</span> <span class="nf">hasAuditLog</span><span class="o">(</span><span class="nc">String</span> <span class="n">eventName</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">isNotNull</span><span class="o">();</span>
        <span class="kt">var</span> <span class="n">logs</span> <span class="o">=</span> <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">AuditRepository</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">findByUserId</span><span class="o">(</span><span class="n">actual</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>

        <span class="c1">// AssertJ does the loop for us and prints a readable error if it does not find the element</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">logs</span><span class="o">)</span>
                <span class="o">.</span><span class="na">extracting</span><span class="o">(</span><span class="nl">AuditLog:</span><span class="o">:</span><span class="n">getEventName</span><span class="o">)</span>
                <span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">eventName</span><span class="o">);</span>

        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="c1">// ... rest of the methods</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Another advantage is a more precise message when the assertion fails. We do not get a generic, meaningless message like
<code class="language-plaintext highlighter-rouge">expected true but was false</code>, but for example:
<code class="language-plaintext highlighter-rouge">Expected logs to contain 'USER_DEACTIVATED' but found ['INITIAL_CREATION', 'LOGIN_SUCCESS']</code></p>

<p>Example usage:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// then</span>
<span class="n">assertThat</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
<span class="o">.</span><span class="na">isDeactivated</span><span class="o">()</span>
<span class="o">.</span><span class="na">hasAuditLog</span><span class="o">(</span><span class="s">"USER_DEACTIVATED"</span><span class="o">)</span>
<span class="o">.</span><span class="na">isProcessedBy</span><span class="o">(</span><span class="s">"AUTH_SERVICE"</span><span class="o">)</span> 
<span class="o">.</span><span class="na">issuedBy</span><span class="o">(</span><span class="s">"ADMIN_123"</span><span class="o">);</span>
</code></pre></div></div>

<hr />

<h3 id="5-domain-specific-language">5. Domain Specific Language</h3>

<p>Can we go even further and try to make the test resemble the actual business requirements, which should be agnostic to
the technologies used? After all, why should the business care whether we store user data in Mongo or in a relational
database? Secondly, new people joining the project can more easily absorb domain knowledge. We can move to a level where
tests not only confirm that our system works according to certain rules, but also become living business documentation,
because the truth is in the code, not in requirements written on <code class="language-plaintext highlighter-rouge">Confluence</code>.</p>

<h4 id="51-interface-with-a-default-implementation-as-the-basic-building-block">5.1 Interface with a default implementation as the basic building block</h4>

<p>Instead of calling a repository, the test “has the ability” to manage users. We will use Ability interfaces for that.
They let us “inject” behavior into the test without cluttering it with <code class="language-plaintext highlighter-rouge">@Autowired</code> annotations or technical
infrastructure code.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">UserAbility</span> <span class="o">{</span>
    <span class="c1">// We hide Spring and the database</span>
    <span class="k">default</span> <span class="nc">User</span> <span class="nf">thereIs</span><span class="o">(</span><span class="nc">UserBuilder</span> <span class="n">builder</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
        <span class="k">return</span> <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">UserRepository</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// Domain: what happens in the system</span>
    <span class="k">default</span> <span class="kt">void</span> <span class="nf">userIsDeactivated</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">,</span> <span class="nc">String</span> <span class="n">reason</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">service</span> <span class="o">=</span> <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">UserService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">user</span><span class="o">.</span><span class="na">deactivate</span><span class="o">(</span><span class="n">reason</span><span class="o">);</span>
        <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">interface</span> <span class="nc">AuditAbility</span> <span class="o">{</span>
    <span class="k">default</span> <span class="kt">void</span> <span class="nf">thereIsAnInitialLogFor</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">log</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">(),</span> <span class="s">"SYSTEM"</span><span class="o">);</span>
        <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">AuditRepository</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">log</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="52-example-usage">5.2 Example usage</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserDeactivationTest</span> <span class="kd">implements</span> <span class="nc">UserAbility</span><span class="o">,</span> <span class="nc">AuditAbility</span> <span class="o">{</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// given</span>
        <span class="kt">var</span> <span class="n">admin</span> <span class="o">=</span> <span class="n">thereIs</span><span class="o">(</span><span class="n">aUser</span><span class="o">().</span><span class="na">withRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">).</span><span class="na">withStatus</span><span class="o">(</span><span class="s">"Active"</span><span class="o">));</span>

        <span class="c1">// and</span>
        <span class="n">thereIsAnInitialLogFor</span><span class="o">(</span><span class="n">admin</span><span class="o">);</span>

        <span class="c1">// when</span>
        <span class="n">userIsDeactivated</span><span class="o">(</span><span class="n">admin</span><span class="o">,</span> <span class="s">"User requested"</span><span class="o">);</span>

        <span class="c1">// then</span>
        <span class="n">assertThatUser</span><span class="o">(</span><span class="n">admin</span><span class="o">)</span>
                <span class="o">.</span><span class="na">isDeactivated</span><span class="o">()</span>
                <span class="o">.</span><span class="na">hasDeactivationReason</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">hasAuditLog</span><span class="o">(</span><span class="s">"STATUS_CHANGE"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">isProcessedBy</span><span class="o">(</span><span class="s">"AUTH_SERVICE"</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>And here is how we can easily pull beans from Spring in tests for the needs of <code class="language-plaintext highlighter-rouge">Ability</code> interfaces, for example:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TestBeanProvider</span> <span class="kd">implements</span> <span class="nc">ApplicationContextAware</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">ApplicationContext</span> <span class="n">context</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setApplicationContext</span><span class="o">(</span><span class="nc">ApplicationContext</span> <span class="n">applicationContext</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">context</span> <span class="o">=</span> <span class="n">applicationContext</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">getBean</span><span class="o">(</span><span class="nc">Class</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">beanClass</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">context</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="n">beanClass</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>It is worth remembering that the solution above, based on the Spring context, is dedicated to integration tests. In pure
unit tests, Ability interfaces can simply receive dependencies through a constructor or use In-Memory implementations,
which I will talk about in the next part.</p>

<hr />

<h4 id="53-what-do-we-gain-from-this-abstraction">5.3 What do we gain from this abstraction?</h4>

<p>The test above is no longer code that only a programmer can understand, but a readable description of system behavior.
Moving to this level of abstraction brings concrete architectural benefits:</p>

<ul>
  <li>
    <p>Technology agnosticism: If a year from now the decision is made to change the database from relational to document, the
test scenario itself remains untouched. You only change the implementation inside <code class="language-plaintext highlighter-rouge">UserAbility</code>, and the business logic
of the test still correctly verifies the system.</p>
  </li>
  <li>
    <p>Protection against outdated documentation: Documentation in Confluence or Jira becomes outdated a second after a task
is closed. A test written this way is an <code class="language-plaintext highlighter-rouge">Executable Specification</code> — a specification that cannot lie, because if it
becomes outdated, the system simply will not pass the <code class="language-plaintext highlighter-rouge">CI/CD</code> process.</p>
  </li>
  <li>
    <p>Faster onboarding: A new developer on the team does not have to analyze which repositories and services are needed to
prepare the database state. They use ready-made “abilities” (<code class="language-plaintext highlighter-rouge">Abilities</code>), so they learn business processes instead of
focusing on technological information noise.</p>
  </li>
  <li>
    <p>Ubiquitous Language: The test code starts to sound like a conversation with the <code class="language-plaintext highlighter-rouge">Product Owner</code>. “There is an admin”,
“User is deactivated” — these are terms everyone understands, not only developers.</p>
  </li>
</ul>

<hr />

<h3 id="summary-part-1">Summary (Part 1)</h3>

<p>A good testing culture is not just a high percentage in a code coverage report. It is primarily trust in your own
solution and the ease of evolving it. In this part, we focused on readability and communication. We moved:</p>

<p>From technical noise and a “wall of text”, through patterns such as <code class="language-plaintext highlighter-rouge">Test Data Builder</code> and <code class="language-plaintext highlighter-rouge">Custom Assertions</code>.</p>

<p>All the way to creating our own Domain DSL, which makes a test become a business specification, not just a piece of code
understood by developers.</p>

<p>Remember: if a test is hard to read, nobody will maintain it. <strong>And a dead test is worse than no test at all</strong>.</p>

<hr />

<h3 id="what-is-next">What is next</h3>

<p>Readability is only half the battle. Even the nicest test will be useless if every second build in the pipeline is red
for no clear reason (<code class="language-plaintext highlighter-rouge">flaky tests</code>), runs slowly, or starts lying to us because we mocked everything around it with for
example Mockito, and debugging it brings no solution.</p>

<p>In the next part, we will talk about:</p>

<ul>
  <li>
    <p><strong>Why I avoid Mockito and test “Black Box”</strong>: I prefer to test real implementations, often with In-Memory versions for
unit tests, instead of writing tests that only verify whether we called a mock.</p>
  </li>
  <li>
    <p><strong>Silent performance killers:</strong> Why annotations such as <code class="language-plaintext highlighter-rouge">@DirtiesContext</code> and <code class="language-plaintext highlighter-rouge">@SpyBean</code> are evil because they make
Spring reload the context over and over again and suddenly make the build much longer.</p>
  </li>
  <li>
    <p><strong>Controlling time:</strong> How to stop fighting <code class="language-plaintext highlighter-rouge">LocalDateTime.now()</code> and start using your own <code class="language-plaintext highlighter-rouge">Clock Provider</code>, so date
tests become predictable.</p>
  </li>
  <li>
    <p><strong>Asynchronicity:</strong> How to get rid of <code class="language-plaintext highlighter-rouge">Thread.sleep()</code> and replace it with <code class="language-plaintext highlighter-rouge">Awaitility</code>, so the test does not wait even
one second too long and becomes more resilient to the passage of time.</p>
  </li>
  <li>
    <p><strong>Isolation and no state</strong>: Why I use a <code class="language-plaintext highlighter-rouge">Database Cleaner</code> instead of <code class="language-plaintext highlighter-rouge">@Transactional</code> annotations on test classes.</p>
  </li>
  <li>
    <p><strong>Infrastructure</strong>: A short introduction to Testcontainers and Wiremock, or how to test with a real database and API
without pretending that “it works on H2 on my machine”.</p>
  </li>
</ul>]]></content><author><name></name></author><category term="java" /><category term="testing" /><category term="spring-boot" /><category term="clean-code" /><summary type="html"><![CDATA[In this article, I want to share how I approach writing tests that are not there only to increase so-called code coverage, but give me confidence that after deploying a new feature, it works according to the original assumptions. Readability Let’s start with the basics. If a test does not clearly communicate what is being tested and why it failed, then all the technology around it becomes unnecessary weight. Many times, while reviewing tests during code review, I really have to make an effort to understand what they are actually testing, without trusting the test name too much.]]></summary></entry><entry xml:lang="pl"><title type="html">Testy, które nie kłamią cz. 1: Czytelność i DSL</title><link href="https://camilyed.github.io//pl/testy-ktore-nie-klamia/" rel="alternate" type="text/html" title="Testy, które nie kłamią cz. 1: Czytelność i DSL" /><published>2026-01-25T00:00:00+00:00</published><updated>2026-01-25T00:00:00+00:00</updated><id>https://camilyed.github.io//pl/testy-ktore-nie-klamia</id><content type="html" xml:base="https://camilyed.github.io//pl/testy-ktore-nie-klamia/"><![CDATA[<p>W tym artykule chcę się podzielić jak podchodzę do pisania testów, które nie są wyłącznie po to aby pokryć tzw.
code-coverage, ale dają mi pewność, że po wdrożeniu nowej funkcjonalności - działa ona zgodnie z pierwotnymi
założeniami.</p>

<h2 id="czytelność">Czytelność</h2>

<p>Zacznijmy od podstaw. Jeśli test nie komunikuje jasno, co jest testowane i dlaczego padł, to cała reszta technologii
staje się niepotrzebnym ciężarem. Wiele razy przeglądając kod dostarczonych testów podczas procesu code review, muszę
naprawdę postarać się zrozumieć, co one faktycznie testują, nie ufając nad zbyt samemu opisowi testu.</p>

<!--more-->

<h3 id="1-struktura-given-when-then">1. Struktura Given-When-Then</h3>

<p>Spójrzmy na pierwszy przykład, który nie jest wcale rozbudowany, ale już na wstępie sprawia, że trzeba nieco bardziej
się wysilić, aby wyłuskać to, co jest na wejściu testu — input, co testujemy — zachowanie, oraz co na końcu sprawdzamy —
asercja.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">updateTest</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">changeStatus</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="nc">User</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertEquals</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">,</span> <span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Proste separatory kodu poprzez np. komentarze — sprawiają, że już przy pierwszym spojrzeniu na test, jest nam łatwiej
się połapać, gdzie tworzymy setup wejściowy - <code class="language-plaintext highlighter-rouge">// given </code>, co testujemy <code class="language-plaintext highlighter-rouge">// when</code>, oraz co weryfikujemy <code class="language-plaintext highlighter-rouge">// then</code>.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldChangeUserStatusToInactive</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="n">user</span><span class="o">.</span><span class="na">changeStatus</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="kt">var</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>W miarę jak nasze testy stają się bardziej realistyczne, sekcje przygotowania danych czy asercji mogą się rozrastać.
Zamiast tworzyć jeden wielki blok kodu pod <code class="language-plaintext highlighter-rouge">// given</code>, warto użyć słowa pomocniczego <code class="language-plaintext highlighter-rouge">// and</code>. Pozwala to logicznie
pogrupować operacje, np. oddzielić tworzenie użytkownika od ustawiania jego uprawnień lub stanu bazy danych.</p>

<p>Spójrzmy na nieco bardziej rozbudowany przykład:</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">complexUpdateTest</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setAddress</span><span class="o">(</span><span class="k">new</span> <span class="nc">Address</span><span class="o">(</span><span class="s">"Warszawa"</span><span class="o">,</span> <span class="s">"Złota 44"</span><span class="o">));</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="nc">AuditLog</span> <span class="n">log</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">auditRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">log</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">changeStatus</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setDeactivationReason</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="nc">User</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertEquals</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">,</span> <span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">());</span>
    <span class="n">assertEquals</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">,</span> <span class="n">updated</span><span class="o">.</span><span class="na">getDeactivationReason</span><span class="o">());</span>
    <span class="n">assertNotNull</span><span class="o">(</span><span class="n">auditRepository</span><span class="o">.</span><span class="na">findByUserIdAndType</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="s">"STATUS_CHANGE"</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Nawet w tak małym teście zaczynamy mrużyć oczy, żeby zrozumieć, co jest tłem, a co akcją. Zastosowanie struktury z <code class="language-plaintext highlighter-rouge">and</code>
znacznie poprawia czytelność:</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">);</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="c1">// and</span>
    <span class="kt">var</span> <span class="n">initialLog</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">auditRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">initialLog</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="n">user</span><span class="o">.</span><span class="na">deactivate</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="kt">var</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getDeactivationReason</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="c1">// and</span>
    <span class="kt">var</span> <span class="n">statusLog</span> <span class="o">=</span> <span class="n">auditRepository</span><span class="o">.</span><span class="na">findByUserIdAndType</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="s">"STATUS_CHANGE"</span><span class="o">);</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">statusLog</span><span class="o">).</span><span class="na">isNotNull</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Czy to już koniec?</strong></p>

<p>Mimo że powyższy kod wygląda znacznie lepiej niż “ściana tekstu”, to wciąż mamy tu sporo szumu technicznego (ręczne
ustawianie pól, settery, techniczne asercje jedna po drugiej).</p>

<p>W kolejnych sekcjach zobaczymy, jak za pomocą wzorców takich jak Test Data Builder oraz Custom Assertions, możemy
sprawić, że ten sam test będzie wyglądał niemal jak zdania w języku naturalnym.</p>

<hr />

<h3 id="2-test-data-builder">2. Test Data Builder</h3>

<p>Wróćmy do naszego „smutnego kodzika” z sekcji wyżej. Dlaczego on tak naprawdę kuje w oczy? Bo za każdym razem, gdy
chcemy stworzyć użytkownika, musimy wywołać konstruktor ze wszystkimi polami albo zestaw setterów. To tworzy ogromny
szum informacyjny. Nie wszystkie przecież dane ustawione w konstruktorze mogą wpływać na wynik asercji, co więcej, jeśli
konstruktor zostałby np. rozszerzony o kolejny argument, nasze testy wymagałyby zmian, w każdym miejscu, gdzie ten
konstruktor wywołujemy, mamy wtedy do czynienia z pojęciem <code class="language-plaintext highlighter-rouge">fragile tests</code>.</p>

<p>Rozwiązaniem na tę sytuację jest wzorzec <strong>Test Data Builder</strong> opisany
przez <a href="http://www.natpryce.com/articles/000714.html">Nat Pryce’a</a>. Ideą jest stworzenie klasy pomocniczej, która posiada
sensowne, domyślne wartości dla wszystkich pól. W samym teście nadpisujemy tylko te parametry, które są kluczowe dla
danego scenariusza.</p>

<p>Zanim przejdziemy do implementacji Buildera, zobaczmy, jak wygląda sekcja <code class="language-plaintext highlighter-rouge">// given</code> w momencie, gdy nasza domena staje
się bogatsza. Załóżmy, że aby zapisać użytkownika w bazie, musimy spełnić szereg wymagań technicznych: adres, dane
kontaktowe, daty, uprawnienia.</p>

<p>W teście deaktywacji te dane to tylko tło, ale w kodzie zajmują pierwszy plan:</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">address</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Address</span><span class="o">(</span><span class="s">"Warszawa"</span><span class="o">,</span> <span class="s">"Złota 44"</span><span class="o">,</span> <span class="s">"00-123"</span><span class="o">,</span> <span class="s">"Polska"</span><span class="o">);</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">,</span> <span class="s">"Kowalski"</span><span class="o">,</span> <span class="s">"jan.k@example.com"</span><span class="o">,</span> <span class="s">"Active"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setAddress</span><span class="o">(</span><span class="n">address</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">);</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setCreatedAt</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">());</span>
    <span class="n">user</span><span class="o">.</span><span class="na">setLastLogin</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">().</span><span class="na">minusDays</span><span class="o">(</span><span class="mi">1</span><span class="o">));</span>
    <span class="n">repository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// and - przygotowanie logów technicznych, które muszą być w systemie</span>
    <span class="kt">var</span> <span class="n">initialLog</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">(),</span> <span class="s">"SYSTEM"</span><span class="o">);</span>
    <span class="n">auditRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">initialLog</span><span class="o">);</span>

    <span class="c1">// when</span>
    <span class="n">user</span><span class="o">.</span><span class="na">deactivate</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">);</span>
    <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>

    <span class="c1">// then - sprawdzamy tylko pole status</span>
    <span class="kt">var</span> <span class="n">updated</span> <span class="o">=</span> <span class="n">repository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">updated</span><span class="o">.</span><span class="na">getStatus</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Inactive"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Mamy tutaj aż 8 linii kodu tylko po to, żeby przygotować obiekt do testu. Czy którykolwiek z tych parametrów ma wpływ na
to, czy administrator zostanie poprawnie zdezaktywowany? Oczywiście, że nie. Skoro te techniczne detale są nieistotne z
punktu widzenia logiki biznesowej deaktywacji, powinny zostać ukryte.</p>

<p>Właśnie tutaj z pomocą przychodzi <code class="language-plaintext highlighter-rouge">Test Data Builder</code>. Pozwala on na zdefiniowanie sensownych, domyślnych wartości dla
wszystkich wymaganych pól w jednym miejscu. Co więcej, możemy pójść krok dalej i zastosować kompozycję builderów. Jeśli
nasz <code class="language-plaintext highlighter-rouge">User</code> posiada <code class="language-plaintext highlighter-rouge">Address</code>, a adres nas w danym teście nie interesuje – builder użytkownika po prostu użyje
domyślnego
buildera adresu.</p>

<p>Spójrzmy na implementację:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserBuilder</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"Jan"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">lastName</span> <span class="o">=</span> <span class="s">"Kowalski"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">status</span> <span class="o">=</span> <span class="s">"Inactive"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">role</span> <span class="o">=</span> <span class="s">"USER"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">deactivationReason</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">AddressBuilder</span> <span class="n">addressBuilder</span> <span class="o">=</span> <span class="nc">AddressBuilder</span><span class="o">.</span><span class="na">anAddress</span><span class="o">();</span>

    <span class="kd">private</span> <span class="nf">UserBuilder</span><span class="o">()</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">UserBuilder</span> <span class="nf">aUser</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">UserBuilder</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withRole</span><span class="o">(</span><span class="nc">String</span> <span class="n">role</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">role</span> <span class="o">=</span> <span class="n">role</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withStatus</span><span class="o">(</span><span class="nc">String</span> <span class="n">status</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">status</span> <span class="o">=</span> <span class="n">status</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">UserBuilder</span> <span class="nf">withAddress</span><span class="o">(</span><span class="nc">AddressBuilder</span> <span class="n">addressBuilder</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">addressBuilder</span> <span class="o">=</span> <span class="n">addressBuilder</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">User</span> <span class="nf">build</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">User</span> <span class="n">user</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">User</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">lastName</span><span class="o">,</span> <span class="n">status</span><span class="o">,</span> <span class="n">role</span><span class="o">);</span>
        <span class="n">user</span><span class="o">.</span><span class="na">setAddress</span><span class="o">(</span><span class="n">addressBuilder</span><span class="o">.</span><span class="na">build</span><span class="o">());</span>
        <span class="n">user</span><span class="o">.</span><span class="na">setDeactivationReason</span><span class="o">(</span><span class="n">deactivationReason</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">user</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AddressBuilder</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">city</span> <span class="o">=</span> <span class="s">"Warszawa"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">street</span> <span class="o">=</span> <span class="s">"Złota 44"</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">AddressBuilder</span> <span class="nf">anAddress</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">AddressBuilder</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">AddressBuilder</span> <span class="nf">withCity</span><span class="o">(</span><span class="nc">String</span> <span class="n">city</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">city</span> <span class="o">=</span> <span class="n">city</span><span class="o">;</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">Address</span> <span class="nf">build</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">Address</span><span class="o">(</span><span class="n">city</span><span class="o">,</span> <span class="n">street</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Co zyskujemy:</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// given</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">aUser</span><span class="o">()</span>
            <span class="o">.</span><span class="na">withRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>

    <span class="c1">// reszta kodu...</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Jak widać, ukryliśmy cały nieistotny szum informacyjny w sekcji given, gdzie skupiamy się jedynie na roli użytkownika,
to ona ma znaczenie w tym teście.</p>

<hr />

<h3 id="3-asercje-najczęstsze-błędy">3. Asercje-najczęstsze błędy</h3>

<p>Mając już idealnie przygotowane dane wejściowe, musimy zadbać o to, by wynik testu faktycznie o czymś nas informował.
Istnieją dwie popularne praktyki, które dają złudne poczucie bezpieczeństwa.</p>

<h4 id="31-współdzielone-zmienne">3.1 Współdzielone zmienne</h4>

<p>Bardzo często kusi nas, aby raz zdefiniowaną wartość (np. imię użytkownika) wykorzystać zarówno w sekcji <code class="language-plaintext highlighter-rouge">// given</code> jak
i w <code class="language-plaintext highlighter-rouge">// then</code>. To błąd. Jeśli przez pomyłkę zmienisz wartość zmiennej na początku testu, asercja na końcu nadal będzie
“zielona”, mimo że system może zachować się błędnie.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">expectedName</span> <span class="o">=</span> <span class="s">"Jan"</span><span class="o">;</span> <span class="c1">// Jeśli tu zmienisz na "Anna"...</span>
<span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">aUser</span><span class="o">().</span><span class="na">withName</span><span class="o">(</span><span class="n">expectedName</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>

<span class="c1">// dalszy kod testu ...</span>

<span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">expectedName</span><span class="o">);</span> 
<span class="c1">// ...test nadal przejdzie!</span>
</code></pre></div></div>

<p>Aby uodpornić powyższy kod na możliwe wystąpienie takiej sytuacji, wystarczy użyć literałów tekstowych.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">aUser</span><span class="o">().</span><span class="na">withName</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>

<span class="c1">// dalszy kod testu ...</span>

<span class="n">assertThat</span><span class="o">(</span><span class="n">result</span><span class="o">.</span><span class="na">getName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan"</span><span class="o">);</span>
</code></pre></div></div>

<h4 id="32-klasy-dto-w-asercjach">3.2 Klasy DTO w asercjach</h4>

<p>W przypadku testów API często można spotkać się z taką praktyką, gdzie w teście odpowiedź z danego endpointu jest
mapowana na klasę DTO odpowiadającej reprezentacji w JSON.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldGetUserDetails</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// when</span>
    <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">UserResponse</span><span class="o">&gt;</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">getForEntity</span><span class="o">(</span><span class="s">"/users/1"</span><span class="o">,</span> <span class="nc">UserResponse</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getStatusCode</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">OK</span><span class="o">);</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">().</span><span class="na">getFullName</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Czemu to jest niezalecana praktyka? Jeśli zmienisz nazwę pola w klasie <code class="language-plaintext highlighter-rouge">UserResponse</code> np. z <code class="language-plaintext highlighter-rouge">fullName</code> na <code class="language-plaintext highlighter-rouge">name</code>, IDE
za pomocą refaktoryzacji automatycznie zaktualizuje tę nazwę również w teście. Wynik jest taki, że test nadal
przechodzi, ale kontrakt endpointu jest już złamany. Jest to częsty przykład testów <code class="language-plaintext highlighter-rouge">False Positive</code>.</p>

<p>W testach integracyjnych warto sprawdzać surową odpowiedź (np. jako String lub mapę) lub użyć biblioteki JsonPath, która
zagląda bezpośrednio w strukturę JSON-a.</p>

<p>🙂</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">shouldGetUserDetailsAndValidateContract</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// pominięty kod, setup testu ustawienie użytkownika w bazie ...</span>

    <span class="c1">// when</span>
    <span class="nc">ResponseEntity</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">getForEntity</span><span class="o">(</span><span class="s">"/users/1"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="c1">// then</span>
    <span class="n">assertThat</span><span class="o">(</span><span class="nc">JsonPath</span><span class="o">.</span><span class="na">read</span><span class="o">(</span><span class="n">response</span><span class="o">.</span><span class="na">getBody</span><span class="o">(),</span> <span class="s">"$.fullName"</span><span class="o">)).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="s">"Jan Kowalski"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h3 id="4-custom-assertion">4. Custom Assertion</h3>

<p>Często w sekcji <code class="language-plaintext highlighter-rouge">// then</code> spotykam kod, który próbuje weryfikować stan systemu po przejściu złożonego procesu. Zamiast
jasnego sygnału, mamy tam logikę, pętle i ręczne wyciąganie danych.</p>

<p>☹️ <strong>Smutny kodzik:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// then</span>
<span class="kt">var</span> <span class="n">logs</span> <span class="o">=</span> <span class="n">auditRepository</span><span class="o">.</span><span class="na">findByUserId</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>

<span class="c1">// Musimy sprawdzić czy w ogóle coś przyszło</span>
<span class="n">assertNotNull</span><span class="o">(</span><span class="n">logs</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="mi">3</span><span class="o">,</span> <span class="n">logs</span><span class="o">.</span><span class="na">size</span><span class="o">());</span>

<span class="c1">// Szukamy konkretnego loga deaktywacji wśród wielu innych</span>
<span class="nc">AuditLog</span> <span class="n">deactivationLog</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>
<span class="k">for</span> <span class="o">(</span><span class="nc">AuditLog</span> <span class="n">log</span> <span class="o">:</span> <span class="n">logs</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="s">"USER_DEACTIVATED"</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">log</span><span class="o">.</span><span class="na">getEventName</span><span class="o">()))</span> <span class="o">{</span>
        <span class="n">deactivationLog</span> <span class="o">=</span> <span class="n">log</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// Sprawdzamy szczegóły - masa technicznych asercji</span>
<span class="n">assertNotNull</span><span class="o">(</span><span class="n">deactivationLog</span><span class="o">,</span> <span class="s">"Log deaktywacji powinien istnieć!"</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"PROCESSED"</span><span class="o">,</span> <span class="n">deactivationLog</span><span class="o">.</span><span class="na">getStatus</span><span class="o">());</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"AUTH_SERVICE"</span><span class="o">,</span> <span class="n">deactivationLog</span><span class="o">.</span><span class="na">getSystemName</span><span class="o">());</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"ADMIN_123"</span><span class="o">,</span> <span class="n">deactivationLog</span><span class="o">.</span><span class="na">getActorId</span><span class="o">());</span>
<span class="n">assertTrue</span><span class="o">(</span><span class="n">deactivationLog</span><span class="o">.</span><span class="na">getTimestamp</span><span class="o">().</span><span class="na">isAfter</span><span class="o">(</span><span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">().</span><span class="na">minusMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">)));</span>

<span class="c1">// I jeszcze sprawdzamy stan pozostałych logów</span>
<span class="n">logs</span><span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="n">l</span> <span class="o">-&gt;</span> <span class="n">assertEquals</span><span class="o">(</span><span class="s">"SUCCESS"</span><span class="o">,</span> <span class="n">l</span><span class="o">.</span><span class="na">getDeliveryStatus</span><span class="o">()));</span>
</code></pre></div></div>

<p>Czemu takie podejście uważam, za złe?</p>

<ol>
  <li>Pętla w teście: Jeśli masz for lub if w teście, to de facto piszesz algorytm. A algorytmy bywają błędne. Czy teraz
potrzebujemy testu do testu?</li>
  <li>Niskopoziomowe detale: Czytając to, musisz, analizować jak działa <code class="language-plaintext highlighter-rouge">for</code>, jak porównujemy Stringi i czy
<code class="language-plaintext highlighter-rouge">assertNotNull</code>
jest w dobrym miejscu. Intencja biznesowa (“użytkownik został zdeaktywowany i odnotowano to w audycie”) ginie w
gąszczu technicznych instrukcji.</li>
  <li>Efekt domina: Jeśli zmieni się struktura loga, musisz poprawić te 15 linii kodu w każdym teście, który go sprawdza.</li>
</ol>

<p>Rozwiązanie: Własna klasa asercji, korzystająca wewnątrz z np. biblioteki <code class="language-plaintext highlighter-rouge">AsserJ</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserAssert</span> <span class="kd">extends</span> <span class="nc">AbstractAssert</span><span class="o">&lt;</span><span class="nc">UserAssert</span><span class="o">,</span> <span class="nc">User</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nc">UserAssert</span> <span class="nf">hasAuditLog</span><span class="o">(</span><span class="nc">String</span> <span class="n">eventName</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">isNotNull</span><span class="o">();</span>
        <span class="kt">var</span> <span class="n">logs</span> <span class="o">=</span> <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">AuditRepository</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">findByUserId</span><span class="o">(</span><span class="n">actual</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>

        <span class="c1">// AssertJ zrobi pętle za nas i wypluje czytelny błąd jeśli nie znajdzie elementu</span>
        <span class="n">assertThat</span><span class="o">(</span><span class="n">logs</span><span class="o">)</span>
                <span class="o">.</span><span class="na">extracting</span><span class="o">(</span><span class="nl">AuditLog:</span><span class="o">:</span><span class="n">getEventName</span><span class="o">)</span>
                <span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">eventName</span><span class="o">);</span>

        <span class="k">return</span> <span class="k">this</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="c1">// ... reszta metod</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Dodatkową zaletą jest bardziej precyzyjny komunikat, kiedy asercja się załamuje, nie dostaniemy ogólnego nic
niemówiącego nam tekstu jak <code class="language-plaintext highlighter-rouge">expected true but was false</code>, ale np.
<code class="language-plaintext highlighter-rouge">Expected logs to contain 'USER_DEACTIVATED' but found ['INITIAL_CREATION', 'LOGIN_SUCCESS']</code></p>

<p>Przykład użycia:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// then</span>
<span class="n">assertThat</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
<span class="o">.</span><span class="na">isDeactivated</span><span class="o">()</span>
<span class="o">.</span><span class="na">hasAuditLog</span><span class="o">(</span><span class="s">"USER_DEACTIVATED"</span><span class="o">)</span>
<span class="o">.</span><span class="na">isProcessedBy</span><span class="o">(</span><span class="s">"AUTH_SERVICE"</span><span class="o">)</span> 
<span class="o">.</span><span class="na">issuedBy</span><span class="o">(</span><span class="s">"ADMIN_123"</span><span class="o">);</span>
</code></pre></div></div>

<hr />

<h3 id="5-domain-specific-language">5. Domain Specific Language</h3>

<p>Czy możemy pójść jeszcze dalej i spróbować doprowadzić do tego, aby test przypomniał faktyczne wymagania biznesowe,
które powinny być agnostyczne wobec zastosowanych technologii, bo co interesuje biznes, że dane użytkownika trzymamy
w Mongo zamiast bazie relacyjnej? Po drugie, nowe osoby dołączające do projektu mogą łatwiej przyswoić sobie wiedzę
domenową, możemy wspiąć się na poziom, gdzie testy nie tylko dostarczają nam potwierdzenia, że nasz system działa wedle
określonych zasad i reguł, ale stanowią jego żywą dokumentację biznesową, ponieważ prawda leży w kodzie, a nie w
wymaganiach spisanych np. na <code class="language-plaintext highlighter-rouge">Confluence</code>.</p>

<h4 id="51-interfejs-z-domyślną-implementacją-jako-podstawowy-building-block">5.1 Interfejs z domyślną implementacją jako podstawowy building block</h4>

<p>Zamiast wołać repozytorium, test “ma zdolność” zarządzania użytkownikami. Wykorzystamy do tego Interfejsy-Zdolności (
<code class="language-plaintext highlighter-rouge">Abilities</code>). Pozwalają one “wstrzykiwać” zachowania do testu bez zaśmiecania go adnotacjami @<code class="language-plaintext highlighter-rouge">Autowired</code> czy
technicznym
kodem infrastruktury.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">UserAbility</span> <span class="o">{</span>
    <span class="c1">// Ukrywamy Springa i bazę danych</span>
    <span class="k">default</span> <span class="nc">User</span> <span class="nf">thereIs</span><span class="o">(</span><span class="nc">UserBuilder</span> <span class="n">builder</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="n">builder</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
        <span class="k">return</span> <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">UserRepository</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// Domena: co się dzieje w systemie</span>
    <span class="k">default</span> <span class="kt">void</span> <span class="nf">userIsDeactivated</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">,</span> <span class="nc">String</span> <span class="n">reason</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">service</span> <span class="o">=</span> <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">UserService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">user</span><span class="o">.</span><span class="na">deactivate</span><span class="o">(</span><span class="n">reason</span><span class="o">);</span>
        <span class="n">service</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">user</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">interface</span> <span class="nc">AuditAbility</span> <span class="o">{</span>
    <span class="k">default</span> <span class="kt">void</span> <span class="nf">thereIsAnInitialLogFor</span><span class="o">(</span><span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">log</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AuditLog</span><span class="o">(</span><span class="s">"INITIAL_CREATION"</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">(),</span> <span class="s">"SYSTEM"</span><span class="o">);</span>
        <span class="nc">TestBeanProvider</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">AuditRepository</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">save</span><span class="o">(</span><span class="n">log</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="52-przykład-użycia">5.2 Przykład użycia</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">UserDeactivationTest</span> <span class="kd">implements</span> <span class="nc">UserAbility</span><span class="o">,</span> <span class="nc">AuditAbility</span> <span class="o">{</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">shouldDeactivateAdminUserAndLogEvent</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// given</span>
        <span class="kt">var</span> <span class="n">admin</span> <span class="o">=</span> <span class="n">thereIs</span><span class="o">(</span><span class="n">aUser</span><span class="o">().</span><span class="na">withRole</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">).</span><span class="na">withStatus</span><span class="o">(</span><span class="s">"Active"</span><span class="o">));</span>

        <span class="c1">// and</span>
        <span class="n">thereIsAnInitialLogFor</span><span class="o">(</span><span class="n">admin</span><span class="o">);</span>

        <span class="c1">// when</span>
        <span class="n">userIsDeactivated</span><span class="o">(</span><span class="n">admin</span><span class="o">,</span> <span class="s">"User requested"</span><span class="o">);</span>

        <span class="c1">// then</span>
        <span class="n">assertThatUser</span><span class="o">(</span><span class="n">admin</span><span class="o">)</span>
                <span class="o">.</span><span class="na">isDeactivated</span><span class="o">()</span>
                <span class="o">.</span><span class="na">hasDeactivationReason</span><span class="o">(</span><span class="s">"User requested"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">hasAuditLog</span><span class="o">(</span><span class="s">"STATUS_CHANGE"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">isProcessedBy</span><span class="o">(</span><span class="s">"AUTH_SERVICE"</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>A o to, jak można łatwo w Springu wyciągać beany w testach na potrzeby np. interfejsów <code class="language-plaintext highlighter-rouge">Ability</code></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TestBeanProvider</span> <span class="kd">implements</span> <span class="nc">ApplicationContextAware</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">ApplicationContext</span> <span class="n">context</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setApplicationContext</span><span class="o">(</span><span class="nc">ApplicationContext</span> <span class="n">applicationContext</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">context</span> <span class="o">=</span> <span class="n">applicationContext</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">getBean</span><span class="o">(</span><span class="nc">Class</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">beanClass</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">context</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="n">beanClass</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Warto pamiętać, że powyższe rozwiązanie oparte na kontekście Springa dedykowane jest dla testów integracyjnych. W
czystych testach jednostkowych (Unit Tests) interfejsy Ability mogą po prostu przyjmować zależności w konstruktorze lub
korzystać z implementacji In-Memory, o których opowiem w kolejnej części.</p>

<hr />

<h4 id="53-co-zyskujemy-dzięki-takiej-abstrakcji">5.3 Co zyskujemy dzięki takiej abstrakcji?</h4>

<p>Powyższy test nie jest już kodem, który zrozumie tylko programista, a czytelnym opisem zachowania systemu. Wyjście na
ten poziom abstrakcji niesie ze sobą konkretne korzyści architektoniczne:</p>

<ul>
  <li>
    <p>Agnostycyzm technologiczny: Jeśli za rok zapadnie decyzja o zmianie bazy danych z relacyjnej na dokumentową, sam
scenariusz testowy pozostanie nietknięty. Zmienisz jedynie implementację wewnątrz <code class="language-plaintext highlighter-rouge">UserAbility</code>, a logika biznesowa
testu nadal będzie poprawnie weryfikować system.</p>
  </li>
  <li>
    <p>Ochrona przed nieaktualną dokumentacją: Dokumentacja na Confluence czy w Jirze starzeje się w sekundę po zamknięciu
zadania. Test napisany w ten sposób to <code class="language-plaintext highlighter-rouge">Executable Specification</code> – specyfikacja, która nie może kłamać, bo jeśli
przestanie
być aktualna, system po prostu nie przejdzie procesu <code class="language-plaintext highlighter-rouge">CI/CD</code>.</p>
  </li>
  <li>
    <p>Szybszy Onboarding: Nowy programista w zespole nie musi analizować, jakie repozytoria i serwisy są potrzebne, by
przygotować stan bazy. Korzysta z gotowych “zdolności” (<code class="language-plaintext highlighter-rouge">Abilities</code>), dzięki czemu uczy się procesów biznesowych, a
nie skupia na technologicznym szumie informacji.</p>
  </li>
  <li>
    <p>Wspólny język (<code class="language-plaintext highlighter-rouge">Ubiquitous Language</code>): Kod testu zaczyna brzmieć tak, jak rozmowa z <code class="language-plaintext highlighter-rouge">Product Ownerem</code>. “There is an
admin”, “User is deactivated” – to terminy, które rozumie każdy, nie tylko deweloperzy.</p>
  </li>
</ul>

<hr />

<h3 id="podsumowanie-część-1">Podsumowanie (Część 1)</h3>

<p>Dobra kultura testowania to nie tylko wysoki procent w raporcie pokrycia kodu. To przede wszystkim zaufanie do własnego
rozwiązania i łatwość jego rozwoju. W tej części skupiliśmy się na czytelności i komunikacji. Przeszliśmy drogę:</p>

<p>Od technicznego szumu i “ściany tekstu”, przez wzorce <code class="language-plaintext highlighter-rouge">Test Data Builder</code> i <code class="language-plaintext highlighter-rouge">Custom Assertions</code>.</p>

<p>Aż po stworzenie własnego Domenowego DSL, który sprawia, że test staje się specyfikacją biznesową, a nie tylko
kawałkiem kodu rozumianego przez programistę.</p>

<p>Pamiętaj: jeśli test trudno się czyta, nikt nie będzie go utrzymywał. <strong>A martwy test jest gorszy niż brak testu</strong>.</p>

<hr />

<h3 id="co-dalej">Co dalej</h3>

<p>Czytelność to dopiero połowa sukcesu. Nawet najładniejszy test będzie bezużyteczny, jeśli co drugi build na pipeline
będzie na czerwono bez wyraźnego powodu (<code class="language-plaintext highlighter-rouge">flaky tests</code>), będzie działał wolno albo zacznie nas oszukiwać przez to, że
wszystko dookoła zamockowaliśmy z użyciem np. Mockito, a jego debugowanie nie przynosi rozwiązania.</p>

<p>W kolejnej części porozmawiamy o:</p>

<ul>
  <li>
    <p><strong>Dlaczego unikam Mockito i testuję “Black Box”</strong>: Wolę testować prawdziwe implementacje (często z wersjami In-Memory
dla unitów), zamiast pisać testy, które weryfikują tylko to, czy wywołaliśmy mocka.</p>
  </li>
  <li>
    <p><strong>Cisi zabójcy wydajności:</strong> Czyli dlaczego adnotacje <code class="language-plaintext highlighter-rouge">@DirtiesContext</code> i <code class="language-plaintext highlighter-rouge">@SpyBean </code>to zło, które sprawia, że Spring
przeładowuje kontekst w kółko i build nagle się wydłuża.</p>
  </li>
  <li>
    <p><strong>Panowanie nad czasem:</strong> Jak przestać walczyć z <code class="language-plaintext highlighter-rouge">LocalDateTime.now()</code> i zacząć używać własnego <code class="language-plaintext highlighter-rouge">Clock Providera</code>,
żeby
testy dat były przewidywalne.</p>
  </li>
  <li>
    <p><strong>Asynchroniczność:</strong> Jak pozbyć się <code class="language-plaintext highlighter-rouge">Thread.sleep()</code> i zastąpić go przez <code class="language-plaintext highlighter-rouge">Awaitility</code>, żeby test nie czekał ani
sekundy za długo i był bardziej kuloodporny na kwestię upływu czasu.</p>
  </li>
  <li>
    <p><strong>Izolacja i brak stanu</strong>: Dlaczego używam <code class="language-plaintext highlighter-rouge">Database Cleanera</code> zamiast adnotacji <code class="language-plaintext highlighter-rouge">@Transactional</code> na klasach
testowych.</p>
  </li>
  <li>
    <p><strong>Infrastruktura</strong>: Krótki wstęp do Testcontainers i Wiremock, czyli jak testować z prawdziwą bazą i API bez udawania,
że “u mnie na H2 działa”.</p>
  </li>
</ul>]]></content><author><name></name></author><category term="java" /><category term="testing" /><category term="spring-boot" /><category term="clean-code" /><summary type="html"><![CDATA[W tym artykule chcę się podzielić jak podchodzę do pisania testów, które nie są wyłącznie po to aby pokryć tzw. code-coverage, ale dają mi pewność, że po wdrożeniu nowej funkcjonalności - działa ona zgodnie z pierwotnymi założeniami. Czytelność Zacznijmy od podstaw. Jeśli test nie komunikuje jasno, co jest testowane i dlaczego padł, to cała reszta technologii staje się niepotrzebnym ciężarem. Wiele razy przeglądając kod dostarczonych testów podczas procesu code review, muszę naprawdę postarać się zrozumieć, co one faktycznie testują, nie ufając nad zbyt samemu opisowi testu.]]></summary></entry></feed>