<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Optivem Journal]]></title><description><![CDATA[TDD | Hexagonal Architecture | Clean Architecture]]></description><link>https://journal.optivem.com</link><image><url>https://substackcdn.com/image/fetch/$s_!0CjJ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9abead4c-3f54-46b1-96aa-7033849416df_200x200.png</url><title>Optivem Journal</title><link>https://journal.optivem.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 05 Jun 2026 17:57:44 GMT</lastBuildDate><atom:link href="https://journal.optivem.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Valentina Jemuović, Optivem]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[optivem@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[optivem@substack.com]]></itunes:email><itunes:name><![CDATA[Valentina Jemuović]]></itunes:name></itunes:owner><itunes:author><![CDATA[Valentina Jemuović]]></itunes:author><googleplay:owner><![CDATA[optivem@substack.com]]></googleplay:owner><googleplay:email><![CDATA[optivem@substack.com]]></googleplay:email><googleplay:author><![CDATA[Valentina Jemuović]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[TDD: Do NOT implement all behaviors at once]]></title><description><![CDATA[Code Demo]]></description><link>https://journal.optivem.com/p/tdd-do-not-implement-all-behaviors-at-once</link><guid isPermaLink="false">https://journal.optivem.com/p/tdd-do-not-implement-all-behaviors-at-once</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 05 Jun 2026 15:11:33 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/81685678-c359-46e2-a6b5-e6b51257fba0_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128197; Join me: <strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong> on Wed 26th Aug, 5:00 - 6:30 PM (CEST)</p><div><hr></div><p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>The mistake is to look at the finished use case &#8212; availability check, number generation, persistence, a response &#8212; and write all of it in one go.</p><p>That&#8217;s not TDD.</p><p>That&#8217;s writing the answer and then sprinkling tests on top.</p><p>TDD is incremental: write the <strong>simplest behavior that could possibly work</strong>, get it green.</p><p>The next failing test decides what gets added next. </p><h2>Behavior 1: a guest can reserve a room</h2><p>Requirement:</p><blockquote><p>&#8220;Guest can reserve a room.&#8221;</p></blockquote><p>Notice what&#8217;s <em>not</em> in that sentence yet: nothing about availability. So we don&#8217;t build availability checking. We build the smallest thing the requirement asks for &#8212; a reservation gets added, and we get a reservation number back.</p><h3><strong>&#128308; RED &#8212; Write the failing test</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;3a336087-7695-40b6-9884-3dd7ddf13547&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">class PlaceReservationTest {

    private FakeReservationRepository reservationRepository;
    private StubReservationNumberGenerator reservationNumberGenerator;
    private PlaceReservationUseCase placeReservationUseCase;

    @BeforeEach
    void setUp() {
        reservationRepository = fakeReservationRepository();            // fake &#8212; in-memory, query state back
        reservationNumberGenerator = stubReservationNumberGenerator();  // stub &#8212; canned number
        placeReservationUseCase = new PlaceReservationUseCase(
            reservationRepository, reservationNumberGenerator);
    }

    @Test
    void createsReservation() {
        reservationNumberGenerator.returns("RES-123");

        var placeReservationRequest = PlaceReservationRequest.builder()
            .guestId("guest-1")
            .roomId("room-101")
            .build();

        var response = placeReservationUseCase.execute(placeReservationRequest);

        assertThat(response.reservationNumber()).isEqualTo("RES-123");

        var reservation = reservationRepository.findByReservationNumber("RES-123");
        assertThat(reservation.guestId()).isEqualTo("guest-1");
        assertThat(reservation.roomId()).isEqualTo("room-101");
    }
}</code></pre></div><p>Notice what exists so far:</p><ul><li><p>repository to add the reservation</p></li><li><p>number generator so the test can pin the returned number (<code>RES-123</code>) deterministically.</p></li></ul><p>There is <strong>no </strong><code>AvailabilityGateway</code> &#8212; nothing requires one yet.</p><p>Look at the request, too: it only has a guest and a room &#8212; <strong>no dates</strong>. A real reservation obviously has dates, but no behavior here <em>uses</em> them yet, so they&#8217;re not in the request yet.</p><p>The requirement was &#8220;reserve a room,&#8221; nothing about <em>when</em>. You add <em>data</em> for the same reason you add code: when a test needs it, not before.</p><h3><strong>&#128994; GREEN &#8212; Make it pass with the simplest code</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;f390ecd7-7aaf-46f5-a87a-61e75c0eb31e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class PlaceReservationUseCase {

    private final ReservationRepository reservationRepository;
    private final ReservationNumberGenerator reservationNumberGenerator;

    public PlaceReservationUseCase(ReservationRepository reservationRepository,
                                   ReservationNumberGenerator reservationNumberGenerator) {
        this.reservationRepository = reservationRepository;
        this.reservationNumberGenerator = reservationNumberGenerator;
    }

    public PlaceReservationResponse execute(PlaceReservationRequest request) {
        var reservationNumber = reservationNumberGenerator.next();

        reservationRepository.add(new Reservation(
            reservationNumber,
            request.guestId(),
            request.roomId()
        ));

        return new PlaceReservationResponse(reservationNumber);
    }
}</code></pre></div><p>Green. It always adds the reservation &#8212; it never checks anything &#8212; because no test has demanded a check yet.</p><p>This feels <em>too</em> simple, and that&#8217;s the point. You are not allowed to add the availability logic now. There&#8217;s no failing test pushing you there.</p><h3><strong>&#128309; REFACTOR &#8212; only if you see the need</strong></h3><p>With the test green, you now get a safe moment to improve the design &#8212; rename, extract, remove duplication &#8212; <em>without changing behavior</em>. You take it <strong>only if you actually see an improvement worth making</strong>; if nothing stands out, you skip it and move on.</p><p>Here the use case is a few straight-line statements with nothing to tidy, so there's nothing to do.</p><h2>Behavior 2: only if the room is available</h2><p>Now a new requirement arrives:</p><blockquote><p>&#8220;A room can only be reserved if it is available for the selected dates.&#8221;</p></blockquote><p><em>Now </em>we need to know if the room is available.</p><h3><strong>&#128308; RED &#8212; Write the failing test</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;d5993566-036f-48df-86ea-2b486e81eb50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">    @Test
    void rejectsReservationWhenRoomIsUnavailable() {
        availabilityGateway
            .forRoom("room-101")
            .from("2026-07-10")
            .to("2026-07-12")
            .returnsUnavailable();

        var placeReservationRequest = PlaceReservationRequest.builder()
            .guestId("guest-1")
            .roomId("room-101")
            .from("2026-07-10")
            .to("2026-07-12")
            .build();

        assertThatThrownBy(() -&gt; placeReservationUseCase.execute(placeReservationRequest))
            .hasMessage("Room is not available");
    }</code></pre></div>
      <p>
          <a href="https://journal.optivem.com/p/tdd-do-not-implement-all-behaviors-at-once">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Batch Integration Is NOT CI/CD]]></title><description><![CDATA[Stop calling it CI/CD if you merge once a week]]></description><link>https://journal.optivem.com/p/batch-integration-is-not-cicd</link><guid isPermaLink="false">https://journal.optivem.com/p/batch-integration-is-not-cicd</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 02 Jun 2026 06:01:23 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1371eafa-1dc8-472a-8dbc-a78f3c424e8b_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BSDe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BSDe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 424w, https://substackcdn.com/image/fetch/$s_!BSDe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 848w, https://substackcdn.com/image/fetch/$s_!BSDe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 1272w, https://substackcdn.com/image/fetch/$s_!BSDe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BSDe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png" width="1025" height="642" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:642,&quot;width&quot;:1025,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:109990,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://journal.optivem.com/i/199647535?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!BSDe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 424w, https://substackcdn.com/image/fetch/$s_!BSDe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 848w, https://substackcdn.com/image/fetch/$s_!BSDe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 1272w, https://substackcdn.com/image/fetch/$s_!BSDe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a350dec-f2ef-455c-8fca-caef18cd05e3_1025x642.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Most teams do the opposite.</p><p>Pain &#8594; delay &#8594; bigger merge &#8594; more pain.</p><p>That is the system.</p><h2>&#8220;We&#8217;ll merge when it&#8217;s done&#8221;</h2><p>Because:</p><ul><li><p>merge is painful</p></li><li><p>integration is slow</p></li><li><p>system breaks in unexpected ways</p></li></ul><p>What teams do next:</p><p>&#10060; integrate less often<br>&#10060; delay merging</p><p>Wrong.</p><h2>1. Integrate into main frequently</h2><p>Not &#8220;eventually&#8221;. Not &#8220;when finished&#8221;.</p><p>&#10004; multiple times per day (or at least daily)<br>&#10004; small changes only</p><p>&#10060; no &#8220;feature branch for 2 weeks&#8221;<br>&#10060; no &#8220;merge at the end of development&#8221;</p><h2>2. Every integration triggers a system check</h2><p>When code is merged (or proposed for merge), the system must verify:</p><p>&#10004; compilation works<br>&#10004; unit tests pass<br>&#10004; component tests pass<br>&#10004; system tests pass</p><p>Not &#8220;some tests&#8221;.</p><p>The goal is:</p><ul><li><p>do the components work in isolation? AND</p></li><li><p>does the system still work as a whole?</p></li></ul><p><em>Note: quick verification (compilation, unit tests &amp; component tests) is triggered on merge, whereas slower verification (system tests) are triggered on an interval-based schedule.</em></p><h2>3. Broken main is not allowed to persist</h2><p>This is the most important rule in real CI.</p><p>&#10004; if main breaks &#8594; fix immediately<br>&#10004; nobody continues building on a broken state<br>&#10004; the team treats main as always releasable</p><p>&#10060; &#8220;we&#8217;ll fix it later&#8221;<br>&#10060; &#8220;let&#8217;s ignore it and keep going&#8221;</p><h2>4. Changes are small by design</h2><p>CI only works if changes stay small.</p><ul><li><p>small commits</p></li><li><p>small merges</p></li><li><p>easy rollback</p></li></ul><p>Because:</p><p>&#10060; large batches hide integration problems<br>&#10004; small batches expose them immediately</p><h2>5. Integration problems are found immediately, not later</h2><p>CI is basically: shorten the time between &#8220;change&#8221; and &#8220;system feedback&#8221;</p><p>So instead of:</p><p>&#10060; change &#8594; days later &#8594; production breaks</p><p>You get:</p><p>&#10004; change &#8594; minutes later &#8594; system check fails</p><h2>6. The system is always in a working state</h2><p>This is the outcome CI is trying to enforce.</p><p>&#10004; main branch always works<br>&#10004; every change is either:</p><ul><li><p>integrated and working</p></li><li><p>or not integrated at all</p></li></ul><p>&#10060; no &#8220;half-working shared state&#8221;</p><h2>What CI/CD is NOT (important)</h2><p>&#10060; running Jenkins<br>&#10060; having a pipeline<br>&#10060; building artifacts<br>&#10060; running tests once per day<br>&#10060; merging occasionally</p><p>Those are tools or schedules.</p><p>Not CI/CD.</p><h2>&#9989; A Real Pipeline = Continuous Delivery</h2><p>&#128073; I&#8217;m running a live workshop where we walk through a working e-shop example with a pipeline architecture, so you can see how it works in practice.</p><p><strong>No rebuild tricks. No &#8220;it passed CI but broke anyway&#8221; surprises.</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop&quot;,&quot;text&quot;:&quot;Join Pipelines Workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop"><span>Join Pipelines Workshop &#8594;</span></a></p><p>&#8364;100 off with code EARLYBIRD100 &#8212; limited spots.</p><p></p><p></p><h5></h5><h2></h2>]]></content:encoded></item><item><title><![CDATA[Clean Architecture: Controllers Should NOT Catch SQL Exceptions]]></title><description><![CDATA[Error Handling - API layer]]></description><link>https://journal.optivem.com/p/clean-architecture-error-handling-api-layer</link><guid isPermaLink="false">https://journal.optivem.com/p/clean-architecture-error-handling-api-layer</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 29 May 2026 14:25:43 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/37ecdadf-8f7d-4131-8262-fa2b9de94335_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><blockquote><p>&#8220;How to handle errors properly?&#8221;</p></blockquote><p>You&#8217;ve seen:</p><ul><li><p><code>SQLException</code> and <code>DataAccessException</code> caught <em>inside controllers</em></p></li><li><p><code>catch (Exception e)</code> returning a hand-rolled <code>500</code></p></li><li><p>giant <code>try/catch</code> blocks wrapping every endpoint</p></li><li><p><code>RuntimeException</code> thrown and swallowed at random</p></li></ul><p>And nobody agrees on the &#8220;right&#8221; way to do it.</p><p>Start where those mistakes happen &#8212; the API layer &#8212; and get one rule right: a controller turns errors into HTTP responses, and it should never reach for a database or framework exception to do it.</p><h2>Error Handling at the API layer</h2><p>This layer should turn errors into HTTP responses.</p><p>Examples:</p><ul><li><p>HTTP status codes</p></li><li><p>error messages</p></li><li><p>API response bodies</p></li></ul><h2>&#10060; Controllers should NOT handle database/framework errors</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;6c3d3463-9464-4781-8777-2fb547cb1a6f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@GetMapping("/orders")
public ResponseEntity&lt;?&gt; getOrders() {

    try {
        return ResponseEntity.ok(orderRepository.findAll());
    } catch (DataAccessException e) {
        return ResponseEntity.status(500).body("Database error");
    }
}</code></pre></div><p>That&#8217;s the wrong place for database exception handling.</p><div><hr></div><p>&#128073; I&#8217;m running a live workshop where we walk through a working e-shop example with a pipeline architecture, so you can see how it works in practice.</p><p><strong>No rebuild tricks. No &#8220;it passed CI but broke anyway&#8221; surprises.</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop&quot;,&quot;text&quot;:&quot;Join Pipelines Workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop"><span>Join Pipelines Workshop &#8594;</span></a></p><p>&#8364;100 off with code EARLYBIRD100 &#8212; limited spots.</p><div><hr></div><h2>&#9989; The API layer should turn errors into responses &#8212; via a global exception handler</h2>
      <p>
          <a href="https://journal.optivem.com/p/clean-architecture-error-handling-api-layer">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Build + Deploy Is NOT a Pipeline]]></title><description><![CDATA[Most teams *think* they have a pipeline.]]></description><link>https://journal.optivem.com/p/build-deploy-is-not-a-pipeline</link><guid isPermaLink="false">https://journal.optivem.com/p/build-deploy-is-not-a-pipeline</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 26 May 2026 06:02:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8d8cfa85-d621-492a-95e8-41a74cf9f329_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Most teams I talk to will tell me they have a pipeline.</p><p>They have GitHub Actions. Or Jenkins. Or Azure DevOps. The build compiles, the Docker image gets pushed, a deploy script runs, and a green checkmark shows up next to the commit. That&#8217;s a pipeline, right?</p><p>It isn&#8217;t.</p><p>What they have is build and deployment automation. The green checkmark only proves the code compiled. Nothing in that verified that the code actually <em>works</em> before the bytes reach a customer.</p><p>I know this because the symptom is always the same.</p><p>The release went out on Tuesday. By Wednesday morning, the support inbox is full &#8212; somebody broke a critical path that nobody tested. Someone stays late. War room reconvenes.</p><p>And it ends with the line every senior engineer has heard a hundred times: <em>&#8220;the pipeline didn&#8217;t catch it.&#8221;</em></p><p>The pipeline didn&#8217;t catch it because there was no pipeline. There was a conveyor belt.</p><h2>A Pipeline Has Checkpoints, NOT Steps</h2><p>Here is the part most teams skip: a pipeline is not a sequence of build steps. It is a sequence of checkpoints.</p><p>Because <strong>steps just run</strong>.</p><p>But <strong>a real pipeline doesn&#8217;t just move code forward</strong>. It asks at every checkpoint:<br>&#8220;Does this build move forward?&#8221;</p><p>If the answer is no, everything stops.</p><p>Most &#8220;pipelines&#8221; don&#8217;t do that.</p><p>They just run jobs until they&#8217;re done and call it success.</p><h2>&#8220;The Pipeline Didn&#8217;t Catch It&#8221;</h2><p>Each stage ran. Each stage passed.</p><p>But there were no tests.<br>So the bugs slipped through.</p><p><strong>A real pipeline tests the system</strong> at each stage:</p><p>&#8220;Does it work on my machine?&#8221;</p><ul><li><p>Commit Stage &#8212; compile, run the fast tests (unit, narrow integration, component, contract), run static analysis, package, publish. Fast. Under five minutes. Triggered on every push.</p></li></ul><p>&#8220;Does it work when deployed?&#8221;</p><ul><li><p>Acceptance Stage &#8212; deploy it and runs system tests (smoke, acceptance, external system contract, E2E).</p></li></ul><p>&#8220;Do we release it?&#8221;</p><ul><li><p>Release Stage &#8212; takes it through QA and into production.</p></li></ul><p>Each stage verifies something different. Each stage can stop the pipeline. The Commit Stage&#8217;s job is not the Acceptance Stage&#8217;s job, and treating them as one big &#8220;CI/CD workflow&#8221; is how you end up with code that &#8220;passed CI&#8221; but breaks the moment it gets deployed.</p><p>If you compile and deploy but never run the deployed code and <strong>check whether the deployed version does what it is supposed to do</strong>, you&#8217;re skipping the most important point.</p><h2>You Didn&#8217;t Deploy the Same Build You Tested</h2><p>Now here&#8217;s the part that quietly breaks most pipelines &#8212; even the ones with good tests.</p><p>The Commit Stage produces a build (e.g. a backend image and a frontend bundle). And from that point on, every stage is supposed to run the same build.</p><p><strong>No rebuilds. No separate &#8220;QA build.&#8221; No special &#8220;production image.&#8221;</strong></p><p>Testing the SAME build makes deployments predictable. "Works in Acceptance" means it will work in Production, because it's the same build.</p><p>The moment you rebuild between stages &#8212; even to swap a config, even to &#8220;just bump the version&#8221; &#8212; the guarantee disappears. The build your tests verified is not what is shipped.</p><p>This is the single most common quiet failure mode I see in real pipelines. The team did the work. They wrote the tests. They setup up the stages. And then somewhere in the middle, a &#8220;for deployment convenience&#8221; rebuild crept in and silently invalidated the whole chain.</p><h2>Is It a Pipeline or Just Automation?</h2><p>If you want to know whether your team has a pipeline or just automation, ask three questions:</p><p><strong>1. What does the green checkmark prove?</strong> </p><p>&#8220;The code compiled&#8221; &#8212; this is a build, not a pipeline. </p><p>&#8220;The code was tested&#8221; &#8212; this is a pipeline.</p><p>&#10060; Just ran<br>&#9989; Verified behavior</p><p><strong>2. What runs between commit and production?</strong></p><p>If the answer is &#8220;compile, package, deploy,&#8221; you&#8217;re missing the Acceptance Stage. The pipeline cannot tell you whether the version works &#8212; only that it built. </p><p>&#10060; Just built<br>&#9989; It works</p><p><strong>3. Are you running the same build in production that you tested earlier?</strong></p><p>If you rebuild between stages, the answer is no. If you cannot tell, the answer is also no.</p><p>&#10060; Different built<br>&#9989; Same build</p><div><hr></div><p>If any of those answers comes back wrong, the green checkmark is lying to you.</p><p>A pipeline is not a deploy script.</p><p>It is a sequence of checkpoints that decide whether a build should move forward.</p><p>If your green checkmark only means &#8220;it compiled and got deployed,&#8221; then you don&#8217;t have a pipeline &#8212; you have a conveyor belt that moves broken code faster.</p><p>And that&#8217;s why release day still feels like a gamble.</p><div><hr></div><p>&#128073; I&#8217;m running a live workshop where we walk through a working e-shop example with a pipeline architecture, so you can see how it works in practice.</p><p><strong>No rebuild tricks. No &#8220;it passed CI but broke anyway&#8221; surprises.</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop&quot;,&quot;text&quot;:&quot;Join Pipelines Workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop"><span>Join Pipelines Workshop &#8594;</span></a></p><p>&#8364;100 off with code EARLYBIRD100 &#8212; limited spots.</p><p></p>]]></content:encoded></item><item><title><![CDATA[DRY is not about *code duplication*]]></title><description><![CDATA[DRY (Don't Repeat Yourself)]]></description><link>https://journal.optivem.com/p/dry-is-not-about-code-duplication</link><guid isPermaLink="false">https://journal.optivem.com/p/dry-is-not-about-code-duplication</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 21 May 2026 06:01:29 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8811ed64-65cf-412b-80ee-01965a9fe81b_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><blockquote><p>&#8220;Never allow similar code to exist.&#8221;</p></blockquote><p>That&#8217;s not DRY.</p><p>The original idea behind DRY wasn&#8217;t &#8220;remove every repeated line.&#8221;</p><p>It was:</p><blockquote><p>Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.</p><p><em>&#8212; Andrew Hunt and David Thomas (The Pragmatic Programmer)</em></p></blockquote><p>Knowledge duplication.</p><p>Not code duplication.</p><div><hr></div><p><strong>Want releases to stop feeling stressful and unpredictable?</strong></p><p>I&#8217;m running a live <a href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop">CI/CD Pipeline Workshop</a> where we build systems that catch issues before production &#8212; not after deployment panic.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop&quot;,&quot;text&quot;:&quot;Reserve My Spot&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop"><span>Reserve My Spot</span></a></p><p><strong>&#8364;100 off</strong> with code <strong>EARLYBIRD100</strong> &#8212; limited spots.</p><div><hr></div><h2>&#10060; Two identical lines are not always duplication </h2><p>Developers see this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;aac0c592-443b-4afe-bbd5-a2dd56cdab62&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class InvoiceTaxCalculator {

    public BigDecimal calculateTax(BigDecimal subtotal) {
        return subtotal.multiply(new BigDecimal("0.20"));
    }
}</code></pre></div><p>&#8230;and then later see this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;cbe03a41-17c0-4b48-87d1-bab059a1a7a0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class ImportDutyCalculator {

    public BigDecimal calculateDuty(BigDecimal subtotal) {
        return subtotal.multiply(new BigDecimal("0.20"));
    }
}</code></pre></div><p>The callers would call:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;24aaf2c7-d33d-4867-a2d7-4a72644069bc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">var tax = invoiceTaxCalculator.calculateTax(subtotal);
var duty = importDutyCalculator.calculateDuty(subtotal);</code></pre></div><p>And immediately someone says:</p><blockquote><p>&#8220;We should extract this into a shared utility.&#8221;</p></blockquote><p>So it gets merged.</p><p>So we decide to get rid of the duplication, by extracting duplicated code here:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;a25ef851-9c79-4217-9a95-88bad9cd2aee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class Calculator {
    public BigDecimal calculate(BigDecimal subtotal) {
        return subtotal.multiply(new BigDecimal(&#8221;0.20&#8221;));
    }
}</code></pre></div><p>We then realize we don&#8217;t need neither <code>InvoiceTaxCalculator</code> nor <code>ImportDutyCalculator</code>, so we delete them too.</p><p>Then, the callers would call:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;329469db-23bf-4960-bbf4-e104dc75c22c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">var tax = calculator.calculate(subtotal);
var duty = calculator.calculate(subtotal);</code></pre></div><p>But the problem is that Invoice Tax and Import Duty calculations are completely unrelated from the business perspective. They have completely different business rules.</p><p>Today, they happen to have the same formula, but that is just incidental.</p><p>Tomorrow, they will end up with different business rules:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;cea8fbc6-724d-443e-84bd-1ce70e08bc29&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class InvoiceTaxCalculator {
    public BigDecimal calculateTax(BigDecimal subtotal, String category) {
        if (category.equals("FOOD")) return subtotal.multiply(new BigDecimal("0.05"));
        if (category.equals("ELECTRONICS")) return subtotal.multiply(new BigDecimal("0.18"));
        return subtotal.multiply(new BigDecimal("0.20"));
    }
}

public class ImportDutyCalculator {
    public BigDecimal calculateDuty(BigDecimal subtotal, String country) {
        if (country.equals("US")) return subtotal.multiply(new BigDecimal("0.25"));
        return subtotal.multiply(new BigDecimal("0.30"));
    }
}</code></pre></div><p>Now the shared abstraction becomes a bug factory.</p><ul><li><p>flags everywhere</p></li><li><p>branching logic explosion</p></li><li><p>&#8220;generic&#8221; calculator that no longer represents a real concept</p></li></ul><p>The duplication was never the code.</p><p>The real mistake was coupling two unrelated business concepts because they happened to look similar at one moment in time.</p><h2>&#9989; DRY is about knowledge, not code</h2>
      <p>
          <a href="https://journal.optivem.com/p/dry-is-not-about-code-duplication">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Jenkins is *not* CI]]></title><description><![CDATA[(Continuous Integration)]]></description><link>https://journal.optivem.com/p/jenkins-is-not-ci</link><guid isPermaLink="false">https://journal.optivem.com/p/jenkins-is-not-ci</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Mon, 18 May 2026 13:17:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a72ee649-61bb-4f65-8226-5eeb4b455414_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Jenkins does NOT equal continuous integration (CI).</p><p>Many teams believe:</p><blockquote><p>&#8220;We have Jenkins, therefore, we are practicing CI.&#8221;</p></blockquote><p>This is wrong.</p><h2>Jenkins is just a tool</h2><p>If a team has Jenkins, but developers work on long-lived branches for days or weeks, then they are not practicing CI.</p><p>If merges happen only before release day, they are not practicing CI.</p><p>If testing is mostly manual, they are not practicing CI.</p><p>If bugs are discovered late, they are not practicing CI.</p><h2>No TDD = no CI</h2><p>Without automated tests you CANNOT:</p><ul><li><p>merge code frequently without fear of breaking things</p></li><li><p>ship small, incremental changes</p></li><li><p>catch regressions early in the pipeline</p></li><li><p>get fast feedback from unit tests</p></li><li><p>verify system behavior with acceptance tests</p></li></ul><p>Releases are big, risky, and infrequent.</p><h2>Do you have CI?</h2><p>Don&#8217;t ask:</p><blockquote><p>&#8220;Do we have Jenkins?&#8221;</p></blockquote><p>Ask:</p><ul><li><p>Do we merge into main daily?</p></li><li><p>Are we protected by automated tests?</p></li><li><p>Do we have Trunk Based Development (TBD) or short-lived branches?</p></li><li><p>Can we merge safely multiple times per day?</p></li><li><p>Is the main branch always releasable?</p></li></ul><p>If the answer is &#8220;no&#8221; to most of these, you don&#8217;t have CI.</p><p>You have a build server.</p><h2>CI in practice</h2><p>This workshop is for <strong>Senior Engineers</strong> &amp; <strong>Tech Leads</strong> who are:<br>&#9642; Stuck with stressful releases<br>&#9642; Stay late fixing production deployments<br>&#9642; See releases break critical paths that nobody tested</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop&quot;,&quot;text&quot;:&quot;Join the CI/CD workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop"><span>Join the CI/CD workshop &#8594;</span></a></p><p><strong>Limited spots.</strong> Register now with the early bird discount - 100 EUR off with code EARLYBIRD100</p>]]></content:encoded></item><item><title><![CDATA[Hexagonal Architecture: The “Microservices First” Mistake]]></title><description><![CDATA[The Distributed Monolith Trap]]></description><link>https://journal.optivem.com/p/hexagonal-architecture-the-microservices-first-mistake</link><guid isPermaLink="false">https://journal.optivem.com/p/hexagonal-architecture-the-microservices-first-mistake</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 15 May 2026 06:01:27 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0a32f553-6b84-46b0-9921-b4db69100fbf_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>You went straight into microservices because&#8230; well, everyone else was doing it.</p><p>&#8220;Scalable.&#8221;<br>&#8220;Cloud-native.&#8221;<br>&#8220;Future-proof.&#8221;<br>&#8220;Independent deployments.&#8221;</p><p>Suddenly there are:</p><ul><li><p>12 services</p></li><li><p>14 repositories</p></li><li><p>RabbitMQ</p></li><li><p>Kafka</p></li><li><p>Kubernetes</p></li></ul><p>&#8230;before you even fully understand the domain.</p><h2>&#10060; The Distributed Monolith Trap</h2><p>You are guessing domain boundaries, so microservice boundaries based on your guesses.</p><p>The problem is you discover, during the next months, that you&#8217;ve modelled the domain in the wrong way. You discover that your microservices are too tightly coupled, and to implement a simple User Story, you have to coordinate changes across multiple microservices. Changes become expensive.</p><p>Immediately jumping to microservices &#8212; especially the wrong microservices &#8212; is expensive to build and expensive to run.</p><p>You can call it &#8220;microservices&#8221;&#8230;</p><p>&#8230;but it&#8217;s just a distributed monolith.</p><h2>&#9888;&#65039;Fear of the Monolith</h2><p>Some developers hear &#8220;monolith&#8221; and imagine:</p><ul><li><p>massive bloated classes</p></li><li><p>tangled dependencies</p></li><li><p>painful future migrations</p></li></ul><p>But not every monolith has to become a &#8220;big ball of mud.&#8221;</p><div><hr></div><p>&#128640; <strong>Register now</strong>: <a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">ATDD &#8211; Acceptance Testing Workshop</a><br>Get 100 EUR off with code <strong>DISCOUNT_100</strong></p><div><hr></div><h2>Hexagonal Architecture Inside the Modules</h2><ul><li><p>Modular Monolith &#8594; all the modules are deployed as a single artifact</p></li><li><p>Microservices &#8594; each microservice is deployed independently</p></li></ul><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fcUr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fcUr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 424w, https://substackcdn.com/image/fetch/$s_!fcUr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 848w, https://substackcdn.com/image/fetch/$s_!fcUr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 1272w, https://substackcdn.com/image/fetch/$s_!fcUr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fcUr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png" width="1456" height="1512" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1512,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4689510,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://journal.optivem.com/i/197195853?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fcUr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 424w, https://substackcdn.com/image/fetch/$s_!fcUr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 848w, https://substackcdn.com/image/fetch/$s_!fcUr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 1272w, https://substackcdn.com/image/fetch/$s_!fcUr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e52bda1-1819-460e-ba58-bd5cdf13ca17_5222x5422.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p>
      <p>
          <a href="https://journal.optivem.com/p/hexagonal-architecture-the-microservices-first-mistake">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Why Developers Hate Meetings]]></title><description><![CDATA[Meetings that go nowhere]]></description><link>https://journal.optivem.com/p/why-developers-hate-meetings</link><guid isPermaLink="false">https://journal.optivem.com/p/why-developers-hate-meetings</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Wed, 13 May 2026 06:02:09 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1902f0df-2ce0-45ff-8a58-136dee9627cb_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>It&#8217;s not because developers hate talking.</p><p>Developers can talk for hours &#8212; just ask them about architecture.</p><h2>The 6-person call where 2 people speak</h2><p>Two people talk the entire time.</p><p>The rest are:</p><ul><li><p>on mute</p></li><li><p>pretending to follow</p></li><li><p>occasionally saying &#8220;yeah&#8221; at the wrong moment</p></li></ul><h2>The developer translation layer</h2><ul><li><p>&#8220;we&#8217;ll figure it out&#8221; &#8594; this is now my problem</p></li><li><p>&#8220;should be simple&#8221; &#8594; it absolutely isn&#8217;t</p></li><li><p>&#8220;just a small change&#8221; &#8594; redesign incoming</p></li><li><p>&#8220;we can iterate&#8221; &#8594; nothing is defined</p></li></ul><p>You get asked for estimates on something that is vaguely described.</p><p>You give a number.</p><p>Everyone writes it down like it&#8217;s a fact.</p><p>Then later the same number becomes:</p><blockquote><p>&#8220;why is this taking longer than expected?&#8221;</p></blockquote><h2>What developers actually need</h2><p>Developers don&#8217;t need more meetings.</p><p>They don&#8217;t need longer discussions either.</p><p>Not 20 minutes of debate.</p><p>Not &#8220;we&#8217;ll figure it out later.&#8221;</p><p>Just enough concrete detail so the work isn&#8217;t being invented mid-implementation.</p><p>A few examples instead of abstract descriptions.</p><p>Most of the frustration comes from building something that wasn&#8217;t fully defined, and then being judged as if it was.</p><h2>Vague requests &#8594; Concrete examples</h2><p>This is <em>exactly</em> where acceptance testing and ATDD help.</p><p>They take requirements that sound like:</p><blockquote><p>&#8220;It should handle refunds properly&#8221;</p></blockquote><p>and turn them into concrete examples developers can implement.</p><p>Acceptance tests turn that into questions like:</p><ul><li><p>Given this input, what should happen?</p></li><li><p>When this happens, what is the expected output?</p></li><li><p>What should <em>not</em> happen?</p></li></ul><div><hr></div><p>&#9889;<strong>Ready to see this in action?</strong></p><p>I&#8217;m running a hands-on <strong><a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">ATDD &#8211; Acceptance Testing Workshop</a></strong> on May 25&#8211;26 (4 hours).</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now - 100 EUR off with code <strong>DISCOUNT_100</strong></p>]]></content:encoded></item><item><title><![CDATA[Most Bugs Start Before Coding]]></title><description><![CDATA[The bug wasn&#8217;t in the code]]></description><link>https://journal.optivem.com/p/most-bugs-start-before-coding</link><guid isPermaLink="false">https://journal.optivem.com/p/most-bugs-start-before-coding</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Mon, 11 May 2026 06:41:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/33eb2340-814f-41f2-bb91-6d19db72eff8_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>I thought bugs came from bad code.</p><p>But I noticed that a lot of the bugs we were fixing in QA&#8230;<br>weren&#8217;t really coding bugs.</p><p>The implementation matched the ticket.<br>The unit tests passed.<br>The code review passed.</p><p>And somehow, the feature was &#8220;wrong behavior.&#8221;</p><p>Not because the code was broken.</p><p>But because people had imagined different things in their mind &#8212; usually in the same meeting where everyone thought they agreed.</p><p>That mismatch only shows up later.</p><p>QA raises issues.</p><p>The product owner says: &#8220;That&#8217;s not what I expected.&#8221;</p><h2>Acceptance Testing Isn&#8217;t Really About Testing</h2><p>Acceptance testing &#8212; or ATDD &#8212; isn&#8217;t just about &#8220;more tests.&#8221;</p><p>It&#8217;s about creating shared understanding before implementation starts.</p><p>It&#8217;s asking questions like:</p><ul><li><p>What should happen here?</p></li><li><p>What does success actually look like?</p></li><li><p>Can we describe this with real examples?</p></li><li><p>What happens in edge cases?</p></li></ul><p>That&#8217;s where a lot of bugs get prevented.</p><p>Not during coding.</p><p>During alignment.</p><h2>&#9940; Without Acceptance Testing</h2><p>The developer gets a ticket: &#8220;Apply a 10% discount to orders over &#8364;100.&#8221;</p><p>The developer reads the ticket and assumes:</p><ul><li><p>discount applies before tax</p></li><li><p>only for logged-in users</p></li><li><p>applies to the entire cart total</p></li></ul><p>QA, later on, tests and assumes:</p><ul><li><p>discount applies after tax</p></li><li><p>also works for guest users</p></li><li><p>applies per item, not cart total</p></li></ul><p>The product owner expected:</p><ul><li><p>discount applies before tax</p></li><li><p>only for logged-in users</p></li><li><p>based on cart total</p></li></ul><p>Same ticket. Three different interpretations.</p><p>And nobody realises until QA finds it &#8212; or worse, after release.</p><h2>&#9989; With Acceptance Testing</h2><p>Before coding starts, the team writes examples like:</p><ul><li><p>Given the customer is logged in, when the customer makes an order of &#8364;120, then a 10% discount is applied before tax</p></li><li><p>Given the customer is logged in, when the customer makes an order of &#8364;80, no discount is applied</p></li><li><p>Given a guest user, when the guest makes an order of &#8364;150, no discount is applied</p></li></ul><p>Suddenly, there&#8217;s no room for interpretation.</p><p>Everyone is reacting to the same examples &#8212; not their own mental version of the feature.</p><p>That&#8217;s the shift.</p><p>Acceptance testing doesn&#8217;t add &#8220;more testing.&#8221;</p><p>It removes guessing before the code is even written.</p><h2>Why Teams Skip This</h2><p>Teams want to jump straight into implementation.</p><p>Writing code feels productive.</p><p>Clarifying expectations sometimes feels like &#8220;extra process.&#8221;</p><p>Until the feature comes back three times for rework.</p><p>Then suddenly those early conversations don&#8217;t seem so expensive anymore.</p><h2>See It In Practice</h2><p>I&#8217;m running a live, hands-on workshop where we build acceptance tests. We go from vague requirements &#8594; to concrete examples &#8594; to executable acceptance tests.</p><p>&#9889;In 4 hours, you&#8217;ll:</p><ul><li><p>Design acceptance tests using DSL + driver architecture</p></li><li><p>Apply ATDD Cycle with AI to write an acceptance test and implement the change</p></li><li><p>Learn how to introduce acceptance testing in your team</p></li></ul><p>&#128197; May 25-26, 2:00-4:00 PM CET</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now and get 100 EUR off with code <strong>DISCOUNT_100</strong></p>]]></content:encoded></item><item><title><![CDATA[Clean Architecture: Do NOT Inject Loggers Everywhere]]></title><description><![CDATA[Code Example]]></description><link>https://journal.optivem.com/p/clean-architecture-do-not-inject-loggers-everywhere</link><guid isPermaLink="false">https://journal.optivem.com/p/clean-architecture-do-not-inject-loggers-everywhere</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 07 May 2026 06:01:18 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/79b41787-cb3a-4231-8d5c-25c0cd0285a2_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Logging injected directly into use cases, services and handlers is justified as:</p><ul><li><p>&#8220;we need debugging&#8221;</p></li><li><p>&#8220;we need traceability&#8221;</p></li><li><p>&#8220;we need observability&#8221;</p></li></ul><p>But&#8230;</p><h2>&#10060;Logger in every use case</h2><ul><li><p>every use case now knows about logging</p></li><li><p>business logic is mixed with runtime reporting</p></li><li><p>logs become part of how code is &#8220;explained&#8221;</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;425f953c-ab2e-49cb-a093-2a839d031481&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">class CreateOrderUseCase {

    private final Logger logger;

    public CreateOrderUseCase(Logger logger) {
        this.logger = logger;
    }

    public OrderResult execute(CreateOrderCommand cmd) {

        logger.info("CreateOrder started");

        Order order = new Order(cmd.userId());

        logger.info("Order created", Map.of(
            "orderId", order.getId()
        ));

        return new OrderResult(order.getId());
    }
}</code></pre></div><h2>&#9888;&#65039;Structured logging doesn&#8217;t fix the real issue</h2><p>After a while, debugging becomes painful, so you switch to structured logging:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;37f17337-3ff0-4a17-bb19-7a01b9d983c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">logger.info("OrderCreated", Map.of(
    "userId", cmd.userId(),
    "orderId", order.getId()
));</code></pre></div><p>This is better:</p><ul><li><p>easier to search</p></li><li><p>consistent format</p></li><li><p>better for tools</p></li></ul><p>But the real problem doesn&#8217;t change.</p><p>You still have logging calls inside every use case.</p><p>You&#8217;ve just improved <em>how logs look</em>.</p><div><hr></div><p>&#128640; <strong>Register now</strong>: <a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">ATDD &#8211; Acceptance Testing Workshop</a><br>Get 100 EUR off with code <strong>DISCOUNT_100</strong></p><div><hr></div><h2>&#9989;Infrastructure Layer: Logging outside the use case</h2>
      <p>
          <a href="https://journal.optivem.com/p/clean-architecture-do-not-inject-loggers-everywhere">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[I Thought I Was a Developer. I Was Just Retesting.]]></title><description><![CDATA[Every change meant retesting]]></description><link>https://journal.optivem.com/p/i-thought-i-was-a-developer-i-was-just-retesting</link><guid isPermaLink="false">https://journal.optivem.com/p/i-thought-i-was-a-developer-i-was-just-retesting</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 05 May 2026 06:00:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0369cb47-628c-4d8c-830c-eb4d2adbec6e_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>I thought software development meant building things.</p><p>Then I got my first job.</p><p>And most of my time wasn&#8217;t building.</p><p>It was <strong>retesting</strong>.</p><p>Fix something &#8594; test it<br>Add something &#8594; test it<br>Change something small &#8594; test everything again</p><p>The system felt fragile.<br>Like it was waiting to break.</p><p>No one called it &#8220;testing&#8221;&#8212;I was a developer, after all.<br>But in reality, I was running the same manual checks again and again just to feel confident enough to move forward.</p><p>It was exhausting.</p><h2>The first breakthrough</h2><p>Then I discovered unit tests.</p><p>Write the test once &#8594; run it forever.</p><p>No more clicking through the same flows.<br>No more guessing what broke.</p><p>If something failed, I knew exactly where.</p><p>It felt like I finally unlocked &#8220;real&#8221; development.</p><h2>Everything passed. Things still broke.</h2><p>But then something didn&#8217;t make sense.</p><p>All my tests were passing.</p><p>And production bugs&#8230;<br>weren&#8217;t really going down.</p><p>That was the confusing part.</p><p>If everything was &#8220;tested&#8221;&#8230;<br>why were things still breaking?</p><p>Here&#8217;s what I was missing:</p><p>Unit tests tell you that <strong>pieces</strong> of your system work.</p><p>They don&#8217;t tell you that the <strong>system works</strong>.</p><h2>You don&#8217;t ship code. You ship behavior.</h2><p>You can have 100% unit test coverage&#8230;<br>and still ship a broken feature.</p><p>Because users don&#8217;t interact with your classes.</p><p>They interact with behavior.</p><p>That&#8217;s what led me to acceptance tests.</p><p>Tests that don&#8217;t care how the code works (you can write them even if your code is a Ball of Mud)&#8212;<br>only whether the system behaves correctly from the outside.</p><p>Like a user would experience it.</p><p>Because in the end:</p><p>You don&#8217;t ship code.<br>You ship behavior.</p><h2>See It in Practice</h2><p>Want to see how acceptance tests can save you time, frustration, and endless bug loops? I&#8217;m running a hands-on <strong><a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">Acceptance Testing Workshop</a></strong> on May 25&#8211;26 (4 hours).</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now - <strong>100 EUR off with code DISCOUNT_100</strong></p><p></p>]]></content:encoded></item><item><title><![CDATA[Stop Duplicating Acceptance Tests]]></title><description><![CDATA[How to avoid double maintenance costs]]></description><link>https://journal.optivem.com/p/stop-duplicating-acceptance-tests</link><guid isPermaLink="false">https://journal.optivem.com/p/stop-duplicating-acceptance-tests</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 30 Apr 2026 06:01:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c5981a40-eea9-44e2-bd83-620c6fb11704_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Most developers test the same behavior twice.</p><p>Once through the API.<br>Once through the UI.</p><p>Two separate tests suites.<br>Two sets of assertions.<br>Double the maintenance costs.</p><p>And sooner or later?&#8230;</p><p>The API tests pass.<br>The UI tests fail randomly.<br>Nobody fully trusts either.</p><h2>&#9888;&#65039;Duplication is risky</h2><p>If you test both channels, you end up with two completely separate test suites:</p><ul><li><p>An API test suite that sends HTTP requests and checks JSON responses</p></li><li><p>A UI test suite that drives a browser with Selenium or Playwright</p></li></ul><p>Same scenarios. No shared structure. No shared assertions.</p><p>When a requirement changes, you update two tests instead of one.</p><p>Worse &#8212; the two suites start contradicting each other.</p><p>The API tests check one set of scenarios.<br>The UI tests check a slightly different set.</p><p>Gaps appear. Bugs hide in the gaps.</p><div><hr></div><p><strong>Want to skip the pain and go straight to the solution?</strong></p><p>I&#8217;m running a live, hands-on workshop where we build acceptance tests that run against both the API and UI &#8212; compile-time safe, IDE-guided and refactor-proof.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now with the early bird discount - <strong>100 EUR off with code EARLYBIRD100 </strong></p><div><hr></div><h2>&#128161;What Changed Everything for Me</h2><p>Instead of writing the same test twice&#8230;</p>
      <p>
          <a href="https://journal.optivem.com/p/stop-duplicating-acceptance-tests">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Hexagonal Architecture: Your Driven Ports Are Leaking Infrastructure]]></title><description><![CDATA[Clean Interfaces, Leaky Abstractions. Your driven ports look clean &#8212; but they're leaking infrastructure into your domain. Your domain is coupled to infrastructure!]]></description><link>https://journal.optivem.com/p/hexagonal-architecture-your-driven-ports-are-leaking-infrastructure</link><guid isPermaLink="false">https://journal.optivem.com/p/hexagonal-architecture-your-driven-ports-are-leaking-infrastructure</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 23 Apr 2026 06:00:39 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/84c48044-cd90-4af2-ad07-77166096b59b_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>You followed the pattern.</p><p>You split your domain from infrastructure.<br>You added interfaces.<br>You called them &#8220;driven ports.&#8221;</p><p>It looks clean.</p><p>But then:</p><ul><li><p>Your tests are full of mocks</p></li><li><p>Refactoring breaks everything</p></li><li><p>Your domain still feels&#8230; tied to something</p></li></ul><p>And you can&#8217;t quite explain why.</p><h2>Driven Ports Gone Wrong</h2><p>Most developers know they need interfaces between their domain and infrastructure. But the mistake isn&#8217;t forgetting to add them.</p><p>It&#8217;s adding them at the wrong level of abstraction.</p><p>Your driven port might look clean on the surface &#8212; but if infrastructure concepts are leaking through, your domain is already compromised.</p><h2>What a Driven Port Should Do</h2><p>A driven port isn&#8217;t:</p><ul><li><p>a wrapper around a framework class</p></li><li><p>a thin layer over a database driver</p></li><li><p>a generic interface that exposes how things work underneath</p></li></ul><p>A driven port is:</p><ul><li><p>something the domain needs to get its work done</p></li><li><p>a boundary expressed in domain language</p></li><li><p>a contract that hides <em>how</em> things are implemented</p></li></ul><p>The key question: <strong>does this interface expose what the domain needs, or how the infrastructure works?</strong></p><div><hr></div><p>&#128640; <strong>Register now</strong>: <a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">Acceptance Testing Workshop</a><br>Get 100 EUR off with code <strong>EARLYBIRD100</strong></p><div><hr></div><h2>&#10060; Driven Ports That Leak Infrastructure</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;a29ca23f-3f48-405a-8a10-3896668130f9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">interface OrderRepository {
    ResultSet executeQuery(String sql);
    void executeBatch(String[] statements);
}

interface PaymentGateway {
    HttpResponse post(String url, Map&lt;String, String&gt; headers, String jsonBody);
}

interface NotificationService {
    void sendRawMessage(String host, int port, String payload);
}

interface ProductCatalog {
    Map&lt;String, AttributeValue&gt; getItem(String tableName, Map&lt;String, AttributeValue&gt; key);
}</code></pre></div><p>What&#8217;s wrong here?</p><ul><li><p><code>OrderRepository</code> exposes SQL &#8212; the domain now knows it&#8217;s a relational database</p></li><li><p><code>PaymentGateway</code> exposes HTTP &#8212; the domain now knows it&#8217;s a REST API</p></li><li><p><code>NotificationService</code> exposes host and port &#8212; the domain now knows about transport protocols</p></li><li><p><code>ProductCatalog</code> exposes <code>AttributeValue</code> &#8212; the domain now knows it&#8217;s DynamoDB</p></li></ul><p>Your domain is shaped by your infrastructure. Change the database, the API, or the message broker &#8212; and your domain code has to change too.</p><p>That&#8217;s the opposite of decoupling.</p>
      <p>
          <a href="https://journal.optivem.com/p/hexagonal-architecture-your-driven-ports-are-leaking-infrastructure">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Unit Tests are NOT enough!]]></title><description><![CDATA[You have 100% coverage. All the unit tests are passing. But then, in production, a horrible bug happened.]]></description><link>https://journal.optivem.com/p/unit-tests-are-not-enough-592</link><guid isPermaLink="false">https://journal.optivem.com/p/unit-tests-are-not-enough-592</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 21 Apr 2026 06:02:43 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f0d842e1-0a1f-49b1-9513-47cb1ac33f8c_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p><strong>The Unit Test Passed. The Customer Was Overcharged.</strong></p><p><strong>Your unit tests can&#8217;t catch this category of bug. Here&#8217;s proof.</strong></p><p>Before, I showed you how Marco&#8217;s team shipped a <a href="https://journal.optivem.com/p/unit-tests-passed-the-bug-shipped">broken tax calculation despite having green tests across the board</a>.</p><p>I received this question:</p><p><em>&#8220;Can you show me the actual code? I want to prove this to my team.&#8221;</em></p><p>So here it is. A concrete example you can drop into any conversation about test strategy.</p><h2>The Setup</h2><p>We have a simple e-commerce system. When a customer places an order, the system needs to look up the tax rate from an external Tax API and calculate the total.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;7184c6d1-86db-4218-8bae-50b25475194b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class TaxGateway {
    private final HttpClient httpClient;

    public double getTaxRate(String countryCode) {
        HttpResponse response = httpClient.get(
            "https://tax-api.example.com/rates/" + countryCode
        );
        JsonObject json = JsonParser.parse(response.getBody());
        return json.getDouble("rate");
    }
}</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;d4d70deb-55b1-407d-a362-fad2363bb3a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">public class OrderService {
    private final TaxGateway taxGateway;

    public Order placeOrder(String countryCode, double unitPrice, int quantity) {
        double basePrice = unitPrice * quantity;
        double taxRate = taxGateway.getTaxRate(countryCode);
        double taxAmount = basePrice * taxRate;
        return new Order(basePrice, taxAmount, basePrice + taxAmount);
    }
}</code></pre></div><p>The code looks correct. <code>OrderService</code> calls <code>TaxGateway</code>, which makes an HTTP call to the external Tax API. Everything is wired up properly.</p><p>But there&#8217;s a hidden bug. The Tax API returns rates as whole numbers &#8212; <code>{ "rate": 8 }</code> for 8%. The <code>TaxGateway</code> reads this value and returns it directly. It should divide by 100 first, but it doesn&#8217;t.</p><h2>The Unit Test: GREEN &#9989;</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;fb847ace-c321-44db-b0a9-bc7aa2b917e6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@Test
void shouldApplyTaxRateFromExternalService() {
    when(taxGatewayMock.getTaxRate("US"))
        .thenReturn(0.08);

    Order order = orderService.placeOrder("US", 20.00, 5);

    assertEquals(100.00, order.getBasePrice());
    assertEquals(8.00, order.getTaxAmount());
    assertEquals(108.00, order.getTotalPrice());
}</code></pre></div><p>This test passes. And it <em>should</em> pass &#8212; given the mock&#8217;s return value, the math is perfect.</p><p>The developer sees green. CI sees green. The PR gets approved.</p><p>But the mock is a lie. It returns <code>0.08</code> because that&#8217;s what the developer <em>assumed</em> <code>TaxGateway</code> would return. The mock replaces the entire <code>TaxGateway</code> &#8212; the HTTP call never happens, the JSON parsing never runs, and the missing <code>/ 100.0</code> is never exposed.</p><p>In production, the customer orders $100 worth of products and gets charged <strong>$900</strong> &#8212; because <code>100 * 8 = 800</code> in tax.</p><p>And here&#8217;s the thing: you can&#8217;t fix this with more unit tests. You could unit test every single class &#8212; <code>OrderService</code>, <code>TaxGateway</code> &#8212; and they would all pass. The bug lives at the boundary between your code and the real world, and unit tests <em>must</em> mock that boundary. If the mock is wrong, every test built on top of it is wrong too.</p><h2>The Acceptance Test: RED &#10060;</h2><p>Now here&#8217;s the same scenario written as an acceptance test:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;40fa9b64-0afc-4664-8041-7d119108da56&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@Test
void shouldCalculateCorrectTotalWithCountryTaxRate() {
    scenario
        .given().product()
            .withUnitPrice("20.00")
        .and().country()
            .withCode("US")
            .withTaxRate("8%")
        .when().placeOrder()
            .withQuantity("5")
            .withCountry("US")
        .then().shouldSucceed()
        .and().order()
            .hasBasePrice("100.00")
            .hasTaxAmount("8.00")
            .hasTotalPrice("108.00");
}</code></pre></div><p>This test runs against the real system &#8212; real backend, real database, real <code>TaxGateway</code> code &#8212; but with a <strong>stub</strong> for the external Tax API. The <code>given().country().withTaxRate("8%")</code> line configures the Tax API Stub to return <code>{ "rate": 8 }</code> &#8212; exactly what the real Tax API would return.</p><p>The real <code>TaxGateway</code> code executes. The HTTP call fires against the Tax API Stub. The JSON is parsed. And the missing <code>/ 100.0</code> is exposed.</p><p><strong>Result: FAILS.</strong></p><pre><code><code>Expected taxAmount: 8.00
Actual taxAmount:   800.00

Expected totalPrice: 108.00
Actual totalPrice:   900.00</code></code></pre><p>The Tax API Stub returned <code>{ "rate": 8 }</code>, just like the real API would. <code>TaxGateway</code> read the value and returned <code>8</code> directly to <code>OrderService</code>, which calculated <code>100 * 8 = 800</code> in tax. In the unit test, the mock hid this by returning <code>0.08</code> directly. In the acceptance test, the real code path runs and the bug is caught.</p><h2>Why Unit Tests Can Never Catch This</h2><p>The bug isn&#8217;t in <code>OrderService</code>. The math is correct. The wiring is correct. The bug is inside <code>TaxGateway</code> &#8212; in how it parses the external API response.</p><p>But the unit test for <code>OrderService</code> mocks out <code>TaxGateway</code> entirely. The HTTP call never fires. The JSON parsing never runs. The bug is invisible.</p><p><strong>The unit test:</strong></p><ul><li><p>Tests <code>OrderService</code> in isolation</p></li><li><p>Replaces <code>TaxGateway</code> with a mock</p></li><li><p>The mock returns what the developer <em>assumes</em> the gateway returns</p></li><li><p>The real HTTP call and JSON parsing never execute</p></li><li><p>Can&#8217;t catch a bug inside the mocked component</p></li></ul><p><strong>The acceptance test:</strong></p><ul><li><p>Tests the full feature against the real system with stubs for external dependencies</p></li><li><p>The real <code>TaxGateway</code> code runs &#8212; HTTP call, JSON parsing, everything</p></li><li><p>The Tax API Stub behaves like the real Tax API &#8212; returning <code>{ "rate": 8 }</code></p></li><li><p>Catches bugs anywhere in the chain, not just in the class under test</p></li></ul><p>Unit tests verify your logic given your assumptions. Acceptance tests verify the feature works regardless of your assumptions. No amount of unit testing can catch a bug that&#8217;s hidden behind a mock.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8jew!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8jew!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 424w, https://substackcdn.com/image/fetch/$s_!8jew!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 848w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1272w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8jew!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png" width="964" height="1142" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1142,&quot;width&quot;:964,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:127085,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://journal.optivem.com/i/191975581?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8jew!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 424w, https://substackcdn.com/image/fetch/$s_!8jew!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 848w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1272w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>But Wait &#8212; How Do We Know the Stub Is Correct?</h2><p>Fair question. If the acceptance test uses a Tax API Stub instead of the real Tax API, couldn&#8217;t the stub itself be wrong?</p><p>That&#8217;s where <strong>contract tests</strong> come in. A contract test runs the same scenarios against both the real Tax API and the Tax API Stub, and verifies they return the same results. If the stub drifts from reality, the contract test fails.</p><p>So the full safety net looks like this:</p><ul><li><p><strong>Unit tests</strong> verify each component&#8217;s logic in isolation</p></li><li><p><strong>Acceptance tests</strong> verify the feature works end-to-end, using stubs for external systems</p></li><li><p><strong>Contract tests</strong> verify the stubs behave like the real external systems</p></li></ul><p>Each layer catches a different category of bug. Skip any one of them, and you have a blind spot.</p><h2>This Isn&#8217;t an Edge Case</h2><p>The bug in our example was a wrong assumption about the API response format. But this is just one of many ways external system integration breaks:</p><ul><li><p><strong>Wrong format assumption</strong> &#8212; The Tax API returns <code>{ "rate": 8 }</code> meaning 8%, but your code treats it as <code>0.08</code>. Your mock returns whatever you assumed, so the unit test passes.</p></li><li><p><strong>Hardcoded value</strong> &#8212; The developer hardcoded <code>return 0.10</code> inside <code>TaxGateway</code> while building the feature, planning to wire up the HTTP call &#8220;later.&#8221; Later never came. The unit test mocks out <code>TaxGateway</code> entirely, so the hardcoded value is never executed.</p></li><li><p><strong>Wrong field</strong> &#8212; The API returns <code>{ "taxRate": 8, "importRate": 12 }</code> and your code reads <code>importRate</code> instead of <code>taxRate</code>. Your mock only returns the field you expected, so the unit test passes.</p></li><li><p><strong>Stale mapping</strong> &#8212; The API used to return <code>{ "rate": 8 }</code> but v2 changed it to <code>{ "tax": { "rate": 8 } }</code>. Your code still reads the top-level field. Your mock still returns the old format, so the unit test passes.</p></li></ul><p>This pattern shows up constantly across all types of external integrations:</p><ul><li><p>A <strong>payment gateway</strong> that returns amounts in cents, but your code treats them as dollars.</p></li><li><p>A <strong>shipping API</strong> that changed its response format in v2, but your mocks still return v1.</p></li><li><p>An <strong>inventory service</strong> that returns <code>"OUT_OF_STOCK"</code> as a string, but your mock returns a boolean <code>false</code>.</p></li><li><p>A <strong>currency API</strong> that returns rates with 6 decimal places, but your mock rounds to 2.</p></li></ul><p>Every one of these passes unit tests &#8212; because the mock <em>is</em> the assumption. If the assumption is wrong, the mock is wrong, and the test is worthless.</p><p>Every one of these ships to production. Every one of these gets caught by acceptance tests.</p><h2>The Takeaway</h2><p>Unit tests answer: <em>&#8220;Does this component do its job correctly, given my assumptions?&#8221;</em></p><p>Acceptance tests answer: <em>&#8220;Does this feature work the way the customer expects?&#8221;</em></p><p>Contract tests answer: <em>&#8220;Are my assumptions about external systems correct?&#8221;</em></p><p>These are fundamentally different questions. If you&#8217;re only asking the first one, you&#8217;re leaving the other two for your customers to answer.</p><h2>Want to learn how to write acceptance tests like this?</h2><p>I&#8217;m running a live workshop: <strong><a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">Stop Shipping Bugs: Acceptance Tests Workshop</a>.</strong></p><p>4 hours. Two evenings. Live on Zoom, with me.</p><p>&#9889; I&#8217;ll walk you through the full architecture &#8212; DSL, Drivers, how it all fits together &#8212; and by the end, you&#8217;ll have written a real acceptance test from scenario to assertion.</p><p>&#128197; May 25-26, 2:00-4:00 PM CET</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now with the early bird discount - <strong>100 EUR off with code EARLYBIRD100</strong></p><p>&#8212; Valentina</p>]]></content:encoded></item><item><title><![CDATA[DDD + Clean Architecture: Where to Put Validation Logic]]></title><description><![CDATA[Should validation go in the API, application layer, or domain?]]></description><link>https://journal.optivem.com/p/ddd-clean-architecture-where-to-put-validation-logic</link><guid isPermaLink="false">https://journal.optivem.com/p/ddd-clean-architecture-where-to-put-validation-logic</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 16 Apr 2026 06:00:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/57dd65ab-d445-4842-98a2-a8975e1a3406_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><blockquote><p>&#8220;Where should validation go?&#8221;</p></blockquote><p>You&#8217;ve seen:</p><ul><li><p>validation in controllers</p></li><li><p>validation in services</p></li><li><p>validation duplicated in multiple places</p></li></ul><p>And no one is really sure what&#8217;s &#8220;correct&#8221;.</p><p>So let&#8217;s make it concrete.</p><h2>1. Validation at the API layer (input checks)</h2><p>This is the first place data enters your system.</p><p>Here you check things like:</p><ul><li><p>required fields exist</p></li><li><p>types are correct</p></li><li><p>format is valid (email, date, UUID, etc.)</p></li></ul><p>Example:</p><ul><li><p><code>email</code> is missing &#8594; reject request</p></li><li><p><code>age</code> is a string &#8594; reject request</p></li><li><p><code>orderDate</code> is <code>"not-a-date"</code> &#8594; reject request</p></li></ul><p>The API layer&#8217;s job is to <strong>fail fast with clear errors</strong> for malformed input, so the application and domain layers can work with clean, well-typed data.</p><h2>2. Validation in the Application Layer (policies - use case rules)</h2><p>This is where most real mistakes happen.</p><p>This layer handles rules like:</p><ul><li><p>cannot create shipment if stock is insufficient</p></li><li><p>order cannot be placed if cart is empty</p></li><li><p>cannot refund payment if transaction is already settled beyond refund window</p></li><li><p>cannot process payment if currency is not supported for merchant</p></li><li><p>cannot cancel order after it has been shipped</p></li><li><p>cannot apply discount code if it is expired or not eligible</p></li></ul><p>These are not input checks anymore.</p><p>They are <strong>policies</strong>. A policy is a rule about whether an action is allowed right now, given the current state of the world.</p><p>Every one of these needs <strong>something outside the aggregate itself</strong> to evaluate &#8212; inventory, the clock, a merchant config, a discount catalog. In DDD terms, they cross the aggregate boundary.</p><p>That&#8217;s why they don&#8217;t belong in the domain object. An <code>Order</code> alone doesn&#8217;t know whether stock exists. The application layer orchestrates: it fetches what&#8217;s needed, evaluates the policy, and either proceeds or rejects.</p><p>Example flow:</p><ul><li><p>API sends a valid, well-formed request</p></li><li><p>application checks the relevant policies</p></li><li><p>if a policy fails &#8594; reject here, before touching the domain</p></li></ul><p>This is where you stop things that are technically valid, but not allowed in this situation.</p><div><hr></div><p>&#9889; <strong>Register now</strong>: <a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">ATDD &#8211; Acceptance Testing Workshop</a><br>Get 100 EUR off with code <strong>EARLYBIRD100</strong></p><div><hr></div><h2>3. Validation in the Domain Layer (invariants)</h2><p>This is the strictest level.</p><p>These are rules that must <em>never</em> be broken, no matter where the code is called from.</p><p>An invariant is a rule that must <strong>always</strong> hold for a given object, no matter who calls it, from where, at what time.</p><p>Examples:</p><ul><li><p>order must always have at least one line item</p></li><li><p>order total cannot be negative</p></li><li><p>payment cannot be &#8220;SUCCESSFUL&#8221; without a transaction reference</p></li><li><p>reserved quantity cannot exceed available stock</p></li><li><p>order status must follow valid transitions: <code>CREATED &#8594; PAID &#8594; SHIPPED &#8594; DELIVERED</code></p></li><li><p>order cannot have <code>SHIPPED</code> status without a tracking number</p></li></ul><p>If this is broken, your system is already inconsistent.</p><p>So this validation:</p><ul><li><p>is enforced when creating or modifying core objects</p></li><li><p>is tied directly to the business model itself</p></li></ul><p><strong>The key principle: the domain never trusts its callers.</strong></p><p>Even if the API already checked something, the domain re-checks any invariant. Why? Because the domain can be invoked from anywhere &#8212; a job, an event, a test, another bounded context. It cannot assume a well-behaved API sits in front of it.</p><h2>What you should NOT do</h2><p>This is where most real-world code goes wrong:</p>
      <p>
          <a href="https://journal.optivem.com/p/ddd-clean-architecture-where-to-put-validation-logic">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Hexagonal Architecture: Ports Are NOT Just Interfaces]]></title><description><![CDATA[Code Example]]></description><link>https://journal.optivem.com/p/hexagonal-architecture-ports-are</link><guid isPermaLink="false">https://journal.optivem.com/p/hexagonal-architecture-ports-are</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 09 Apr 2026 06:02:31 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0006a123-2c1a-41fd-9df9-a85f7e798fc5_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>When developers hear &#8220;port,&#8221; most think interface.</p><p>Add an interface, call it a port, check the box.</p><p>But here&#8217;s the trap: <strong>just adding interfaces doesn&#8217;t make your domain independent.<br></strong>Abstraction alone doesn&#8217;t automatically enforce architecture.</p><p>If your dependencies point the wrong way, your domain depends on the database, API, or external services &#8212; instead of defining what the system should do.</p><p>In Hexagonal Architecture, the question isn&#8217;t just &#8220;do I have a port?&#8221;</p><p>It&#8217;s: <strong>who does the domain call, and who implements it?</strong></p><p>Do your dependencies point the right way?</p><div><hr></div><p>&#128640; <strong>Register now</strong>: <a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">Acceptance Testing Workshop</a><br>Get 100 EUR off with code <strong>EARLYBIRD100</strong></p><div><hr></div><h2>&#10060; Domain depends on Infrastructure</h2><ul><li><p>Domain directly uses infrastructure classes like <code>JpaOrderRepository</code> or <code>PaymentApiClient</code></p></li><li><p>Business logic becomes tied to storage or APIs</p></li><li><p>Changing the database or API can break domain code</p></li></ul><h2><strong>&#9989;</strong> Domain defines the contract, infrastructure implements the contract</h2><ul><li><p>Domain defines driven ports &#8212; interfaces that describe what it needs done</p></li><li><p>Driven adapters implement the details</p></li><li><p>Domain logic is stable, infrastructure can change without affecting it</p></li></ul><p>Driven ports exist to control this dependency direction.</p><p>Abstraction alone doesn&#8217;t enforce architecture &#8212; direction does.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IiW-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IiW-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 424w, https://substackcdn.com/image/fetch/$s_!IiW-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 848w, https://substackcdn.com/image/fetch/$s_!IiW-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 1272w, https://substackcdn.com/image/fetch/$s_!IiW-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IiW-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png" width="1025" height="1264" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1264,&quot;width&quot;:1025,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:143675,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://journal.optivem.com/i/193263907?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IiW-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 424w, https://substackcdn.com/image/fetch/$s_!IiW-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 848w, https://substackcdn.com/image/fetch/$s_!IiW-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 1272w, https://substackcdn.com/image/fetch/$s_!IiW-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7fc230-cf10-4ca2-8a38-afe964b0307f_1025x1264.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h1>Real-life Example: Ordering System</h1><p>Applying Hexagonal Architecture on the backend (for an e-commerce system).</p>
      <p>
          <a href="https://journal.optivem.com/p/hexagonal-architecture-ports-are">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[TDD: If Your Test Never Fails, It’s Broken]]></title><description><![CDATA[The RED step]]></description><link>https://journal.optivem.com/p/tdd-if-your-test-never-fails-its-broken</link><guid isPermaLink="false">https://journal.optivem.com/p/tdd-if-your-test-never-fails-its-broken</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 07 Apr 2026 06:02:18 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ec518127-dfa2-4e37-993b-d671b8e93624_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Now you&#8217;ve written a test.</p><p>But does it fail?</p><p>If it doesn&#8217;t, you have no proof that your implementation makes a difference.</p><p>A test that always passes is worse than no test.</p><p>It gives you a false sense of security.<br>It hides bugs instead of exposing them.</p><p>That&#8217;s why the <strong>RED step matters</strong>.</p><p>You watch it fail.</p><p>That failure is evidence.</p><p>Later, when it turns GREEN, you know the code caused it &#8212; not luck, not a misconfigured test, not a false positive.</p><h2>&#9889;Can the Test Actually Fail?</h2><p>If you&#8217;ve seen it fail:</p><ul><li><p>It confirms the test is valid.</p></li><li><p>It means that the later GREEN step actually proves your code works.</p></li><li><p>It protects you from false positives &#8212; those sneaky tests that always pass, giving you a fake sense of correctness.</p></li></ul><p>TDD makes it unavoidable.</p><p>No RED? No trust in your code.</p><h2>Why Developers Skip It (And Regret It)</h2><p>It&#8217;s tempting to skip RED:</p><ul><li><p>&#8220;I know this code will work, so why watch it fail?&#8221;</p></li><li><p>&#8220;It&#8217;s faster to just implement and see GREEN.&#8221;</p></li></ul><p>But skipping RED is like installing a fire alarm that never goes off &#8212; you won&#8217;t know something&#8217;s broken until it&#8217;s too late.</p><p>Watching the test fail is the <strong>cheapest, fastest bug prevention you can get</strong>.</p><h2>RED Before GREEN</h2><p>TDD isn&#8217;t just about writing tests.</p><p>It&#8217;s about <strong>proof</strong>.</p><p>Proof that your test matters.<br>Proof that your code works.<br>Proof that the system behaves as expected.</p><p>You can take a step further with Acceptance Test&#8211;Driven Development (ATDD).</p><p>Instead of just asking:</p><blockquote><p>&#8220;Does my code work?&#8221;</p></blockquote><p>ATDD asks:</p><blockquote><p>&#8220;Does the system behave as the user expects?&#8221;</p></blockquote><p>It&#8217;s less about writing more tests and more about making sure everyone &#8212; Devs, QA, and Product Owners &#8212; agrees on what &#8220;done&#8221; really means.</p><p>The cool part? Fewer arguments about whether something is broken, fewer regression bugs, and a lot less late-night firefighting &#8212; because expectations are clear <strong>before you write a single line of code</strong>.</p><div><hr></div><p>&#128640; <strong>Join me for: <a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">ATDD &#8211; Acceptance Testing Workshop</a></strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Get <strong>100 EUR off with code EARLYBIRD100</strong></p><p></p>]]></content:encoded></item><item><title><![CDATA[Clean Architecture: DTOs Are NOT Just Wrappers]]></title><description><![CDATA[Code Example]]></description><link>https://journal.optivem.com/p/clean-architecture-dtos-are-not-just-wrappers</link><guid isPermaLink="false">https://journal.optivem.com/p/clean-architecture-dtos-are-not-just-wrappers</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 03 Apr 2026 06:01:42 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/983a1287-3fd1-45a2-8765-55c7ffe65c66_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Developers like DTOs. And wrappers. And more wrappers.</p><p>Until your &#8220;simple&#8221; API layer turns into <strong>a tower of classes no one can read</strong>, tests become painful, and adding a new field feels like a chore.</p><p>DTOs and mappers are tools &#8212; not dogma. Clean Architecture doesn&#8217;t say &#8220;wrap everything 5 times.&#8221; It says: be intentional about what crosses boundaries.</p><h2>The Problem</h2><p>Many codebases follow a pattern like this:</p><pre><code><code>Domain objects
   &#8595;
Domain DTO
   &#8595;
Application DTO
   &#8595;
API DTO
   &#8595;
JSON</code></code></pre><p>That&#8217;s 3&#8211;4 layers of wrapping <strong>for the same data</strong>, plus mappers everywhere.</p><p>To be clear &#8212; sometimes multiple layers <em>are</em> justified. If you're serving both GraphQL and REST from the same domain, or maintaining a published API contract that must change independently from your domain, extra DTOs earn their keep. The problem isn't layers &#8212; it's layers that exist "just in case."</p><p><strong>The result:</strong></p><ul><li><p>Lots of boilerplate</p></li><li><p>Hard to understand what actually changes the domain</p></li><li><p>Fragile tests because every layer must be mocked</p></li><li><p>Accidental complexity: devs can&#8217;t tell which layer matters for business rules</p></li></ul><div><hr></div><p>&#128640; <strong>Register now</strong>: <a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">Acceptance Testing Workshop</a><br>Get 100 EUR off with code <strong>EARLYBIRD100</strong></p><div><hr></div><h1>&#128161;Code Example</h1><h2>Over-Wrapping Everything &#10060;</h2>
      <p>
          <a href="https://journal.optivem.com/p/clean-architecture-dtos-are-not-just-wrappers">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Clean Code Is NOT Optional]]></title><description><![CDATA[It&#8217;s tempting to write code fast...]]></description><link>https://journal.optivem.com/p/clean-code-is-not-optional</link><guid isPermaLink="false">https://journal.optivem.com/p/clean-code-is-not-optional</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Wed, 01 Apr 2026 06:02:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e5f34d3e-bd39-4e7d-ad18-2cd0c5357885_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>&#128075; <em>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>It&#8217;s tempting to write code fast and plan to clean it up later.</p><p>Skip the naming.<br>Duplicate a bit of logic.<br>Add &#8220;just one more if&#8221;.</p><h2>Messy Code Is a Tax</h2><p>Every change on top of messy code costs more:</p><ul><li><p><strong>Features take longer</strong> because you can&#8217;t trust what&#8217;s already there. You read more than you write, trying to figure out what the code actually does.</p></li><li><p><strong>Bugs hide</strong> in unclear abstractions and duplicated logic. You fix something in one place and miss the copy-pasted version three files over.</p></li><li><p><strong>Refactoring is painful</strong> because dependencies are tangled. You pull one thread and everything unravels.</p></li></ul><p>So you slow down. Every change gets harder. Every decision gets riskier. Every task takes longer than it should.</p><p>And the worst part? You <em>know</em> you should fix it, but now you can&#8217;t justify the time &#8212; because there&#8217;s a backlog of features waiting.</p><p>This is how codebases rot. Not from one bad decision, but from hundreds of small ones that compound.</p><blockquote><p>No matter how slow you are writing clean code, you will always be slower if you make a mess. &#8212; Uncle Bob</p></blockquote><p>Clean code may feel slower at first. But every line you write on top of it is faster, safer, and less stressful.</p><p>The hard truth: Messy code always costs more time than taking the time to do it right.</p><h2>But How Do You Keep Code Clean &#8212; and Keep It Working?</h2><p>Say you&#8217;ve written clean, well-structured code. You refactor a service, rename some methods, extract a class. The code reads well. You&#8217;re confident it&#8217;s correct.</p><p>But is it?</p><p>How do you know that the order flow still works end to end? That the payment integration didn&#8217;t break? That a customer can still check out?</p><p>You don&#8217;t &#8212; unless you have tests that verify <em>behavior</em>, not implementation.</p><h2>Clean Code Needs a Safety Net</h2><p>Unit tests are fast. They give you quick feedback and make refactoring feel safe.</p><p>But even the best unit tests have a blind spot.</p><p>They verify pieces in isolation. They mock the database, stub the API, skip the serialization. So when everything passes, you know your <em>unit</em> <em>logic</em> works &#8212; but you don&#8217;t know if the <em>system logic</em> works.</p><p>The gaps live at the boundaries: the database query that returns data in a different shape than you assumed. The HTTP layer that silently drops a field. The integration that worked in isolation but breaks when it meets real I/O.</p><p>Unit tests can&#8217;t catch these problems &#8212; not because they&#8217;re poorly written, but because that&#8217;s not what they&#8217;re designed to do.</p><p>Acceptance tests fill that gap. They exercise the system end-to-end, through the same boundaries your users hit:</p><p>A <strong>unit test</strong> says: &#8220;This function calculates the discount correctly.&#8221;</p><p>An <strong>acceptance test</strong> says: &#8220;When a customer applies a coupon at checkout, the total price is reduced and the order goes through.&#8221;</p><p>They&#8217;re slower than unit tests. You won&#8217;t run them on every keystroke. But they answer the question that unit tests can&#8217;t:</p><p><strong>Does this actually work for the user?</strong></p><h2>Clean Code Without Acceptance Tests Is a Risk</h2><p>Clean code keeps you moving fast. Unit tests ensure your refactoring doesn&#8217;t break the unit's logic. So why do you need anything else?</p><p>Because unit tests verify your code works <em>in isolation</em>. Acceptance tests verify your system works <em>in reality</em>.</p><p>You can have clean code, a full suite of passing unit tests, and still deploy a broken checkout flow &#8212; because the bug lives at a boundary that no unit test touches.</p><ul><li><p><strong>Clean code</strong> lets you move fast.</p></li><li><p><strong>Unit tests</strong> make sure the pieces work.</p></li><li><p><strong>Acceptance tests</strong> make sure the system works.</p></li></ul><p>One without the other will eventually slow you down.</p><p>You need all three.</p><h2>Where do you start?</h2><p>If you&#8217;re working in a greenfield project, you can build all three from day one.</p><p>But what about legacy code &#8212; where there are no tests, and the code is too messy to unit test?</p><p>You don&#8217;t start with unit tests. You can&#8217;t &#8212; the code isn&#8217;t structured for them yet.</p><p>You start with acceptance tests.</p><p>They don&#8217;t need clean code. They don&#8217;t need dependency injection or isolated modules. They test the system from the outside, through the same entry points your users hit. That means you can add them to any codebase, no matter how messy.</p><p>Once acceptance tests are in place, you have a safety net. Now you can refactor toward clean code and add unit tests &#8212; without breaking what already works.</p><p>Ready to put this into practice?</p><p>I&#8217;m running a hands-on <strong><a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">Acceptance Testing Workshop</a></strong> on May 27&#8211;28 (4 hours).</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now with the early bird discount - <strong>100 EUR off with code EARLYBIRD100</strong></p>]]></content:encoded></item><item><title><![CDATA[TDD & ATDD]]></title><description><![CDATA[In Microservice Architecture]]></description><link>https://journal.optivem.com/p/tdd-and-atdd-in-microservice-architecture</link><guid isPermaLink="false">https://journal.optivem.com/p/tdd-and-atdd-in-microservice-architecture</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 27 Mar 2026 07:02:39 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5beed640-9a0c-4081-b7db-37b6bcb7f5d7_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>&#128274; Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply <a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://journal.optivem.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://journal.optivem.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>When I first got into TDD, I thought I had it figured out.</p><p>I was reading Kent Beck, Martin Fowler&#8230; all of it just made sense.</p><p>I was really into clean code and unit tests.</p><p>I knew about e2e testing, but I saw it as QA&#8217;s job, not mine. Those tests were really badly written and constantly breaking. And to be honest, I saw it as boring and something that developers shouldn&#8217;t be doing.</p><p>I saw the whole CI/CD pipeline as a separate thing done by a DevOps engineer &#8212; not me.</p><div><hr></div><p>Still, no matter how many unit tests were written, management kept on complaining:</p><blockquote><p>&#8220;Why are there still so many bugs?&#8221;</p><p>&#8220;Why is everything taking so long?&#8221;</p><p>&#8220;I thought the last release fixed this!&#8221;</p><p>&#8220;Customers are frustrated &#8212; something has to change!&#8221;</p></blockquote><div><hr></div><p><strong>&#9889;Want to skip the pain and go straight to the solution?</strong></p><p><strong>Every Release Is a Nightmare</strong> doesn&#8217;t have to be your reality.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now with the early bird discount - <strong>100 EUR off with code EARLYBIRD100</strong></p><div><hr></div><h2>A Story That Stuck With Me</h2><p>Later, I stumbled across something Dave Farley shared. The teams were practicing TDD&#8230; they were lots of unit tests, all green &#8212; but the system was failing in QA environment, for possibly weeks. QA engineers constantly had to play catch-up with their e2e tests.</p><p>And that&#8217;s what got Dave Farley to develop acceptance tests.</p><div><hr></div><p>I realized I had to see the bigger picture.</p><p>That meant looking at the whole continuous delivery pipeline and understanding how all the pieces fit together.</p><p>In the commit stage, we&#8217;re doing TDD and writing unit tests (and component tests)&#8212; making sure each component behaves the way we expect.</p><p>But further down the pipeline, there&#8217;s the acceptance stage, where ATDD comes in &#8212; checking that all those pieces actually work together as a system.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u0K5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u0K5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 424w, https://substackcdn.com/image/fetch/$s_!u0K5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 848w, https://substackcdn.com/image/fetch/$s_!u0K5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 1272w, https://substackcdn.com/image/fetch/$s_!u0K5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u0K5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png" width="900" height="1110" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1110,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:120470,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://journal.optivem.com/i/191875686?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!u0K5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 424w, https://substackcdn.com/image/fetch/$s_!u0K5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 848w, https://substackcdn.com/image/fetch/$s_!u0K5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 1272w, https://substackcdn.com/image/fetch/$s_!u0K5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea3036e5-1075-443d-a679-551a9bf49e30_900x1110.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h1>&#9889;TDD &amp; ATDD in Microservice Architecture</h1><h2>ATDD: Seeing the System as a Whole</h2><p>If you zoom out and look at the system&#8230; it starts with a user story.</p><p>And that story comes with <strong>acceptance criteria</strong> &#8212; the conditions that tell you: <em>did we actually deliver what the user needs?</em></p><p>Instead of just reading those criteria&#8230; we can turn each one into an <strong>acceptance test</strong>. It&#8217;s basically like 1:1 mapping.</p><p>And these tests are very different from unit tests.</p>
      <p>
          <a href="https://journal.optivem.com/p/tdd-and-atdd-in-microservice-architecture">
              Read more
          </a>
      </p>
   ]]></content:encoded></item></channel></rss>