<?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>Wed, 05 Aug 2026 04:55:01 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[Nobody Wanted to Write Tests]]></title><description><![CDATA[&#8220;We don&#8217;t have time to write the code twice.&#8221;]]></description><link>https://journal.optivem.com/p/nobody-wanted-to-write-tests</link><guid isPermaLink="false">https://journal.optivem.com/p/nobody-wanted-to-write-tests</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 04 Aug 2026 06:02:15 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e2d564f4-fd9a-4253-b02e-b5c1b3fead4f_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>When I started my career in software development, I thought I was surrounded by the wrong people.</p><p><strong>Nobody wanted to write tests.</strong></p><p><strong>Nobody cared about clean code.</strong></p><p>Every conversation about improving the codebase turned into an argument.</p><p>I remember thinking:</p><p>&#8220;If I just join a better company, things will be different.&#8221;</p><h2>The next company was supposed to be different</h2><p>I moved to another company.</p><p>Surely this time it would be different.</p><p>More experienced developers.</p><p>Better engineers.</p><p>Except...</p><p>The same problems were there.</p><blockquote><p>&#8220;Tests are a waste of time.&#8221;</p><p>&#8220;We&#8217;ll clean up the code later.&#8221;</p><p>&#8220;We have always done it this way.&#8221;</p></blockquote><h2>Being a Senior Developer didn&#8217;t change anything</h2><p>As a Senior Developer, I though:</p><blockquote><p>&#8220;Now I can finally influence how we build software.&#8221;</p><p>We would write better code.</p><p>We would add tests.</p><p>We would stop rushing changes into production.</p></blockquote><p>I was wrong.</p><p>Even getting people to write one test was a battle.</p><blockquote><p>&#8220;Why would we write twice as much code?&#8221;</p><p>&#8220;Now we have to maintain the application code and all the tests too?&#8221;</p></blockquote><h2>The smaller company as a Tech Lead</h2><p>The next move was to a smaller company, as a Team Lead.</p><p>This time I did things differently.</p><p>I <strong>stopped waiting for permission</strong>. In my own time, over several months, I built a Clean Architecture template &#8212; layered, testable, with unit tests already in place.</p><p>When I recruited developers for the team, I <strong>stopped screening only for years of experience</strong>. I screened for whether they cared about quality.</p><p>Then I built the first module with the template myself, end to end, so there was something real to point at.</p><p>And then I trained the team to do it the same way.</p><h2>Six months later</h2><p>Within six months, that team had shipped more features than the &#8220;senior&#8221; developers at my previous companies had managed in years.</p><p>Same industry. Same kind of problems.</p><p>The difference wasn&#8217;t that these developers were smarter. It was that nobody had to be convinced. The template made the right way the easy way, the hiring made the standard shared, and <strong>the training made it something the whole team could do</strong> &#8212; not something one person kept arguing for.</p><p>That&#8217;s what I had been getting wrong for years.</p><p>You don&#8217;t change a team by caring harder than everyone else. You change it by <strong>building the thing, showing it works</strong>, and bringing people with you.</p><p>The template was what made that possible. So that&#8217;s what I want to show you how to design.</p><p>Join the live training session:</p><p><strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong></p><p>&#128467; Aug 26<br>&#9200; 5:00&#8211;6:30 PM (CEST)</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942;Register now&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/live_events/clean-architecture-for-backend-developers"><span>&#127942;Register now</span></a></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Fragile Unit Tests in Clean Architecture]]></title><description><![CDATA[So tightly coupled to the domain that they break on every refactor - even when behavior never changes]]></description><link>https://journal.optivem.com/p/fragile-unit-tests-in-clean-architecture</link><guid isPermaLink="false">https://journal.optivem.com/p/fragile-unit-tests-in-clean-architecture</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 31 Jul 2026 06:01:03 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d1843b9b-a5e3-499b-87b7-fe60c121f3d4_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><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942; Register now&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/live_events/clean-architecture-for-backend-developers"><span>&#127942; Register now</span></a></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><h2>How Uncle Bob writes unit tests in Clean Architecture</h2><p>Years ago, I wrote unit tests in Clean Architecture the way Uncle Bob does. Unit tests target use cases and verify the stateful outcomes on repositories and gateways through test doubles.</p><p>In the Clean Coders <a href="https://cleancoders.com/episode/comparativeDesign-episode-1">comparative design series</a>, Uncle Bob shows that he writes <a href="https://github.com/sandromancuso/cleancoders_openchat/tree/openchat-unclebob/src/test/java/org/openchat/usecases">unit tests targeting use cases</a>, not targeting the domain. That&#8217;s why for each use case class he has a corresponding unit test class - a 1:1 mapping between use cases and unit tests.</p><p><em>You&#8217;ll notice that even though he targets use cases and not the domain, the tests are still coupled to the domain - they assert directly on domain entities. That turns out to be a challenge, as we&#8217;ll see later in this article.</em></p><p>E.g. for the use case <code>PostDocument</code> here&#8217;s the unit test <code>PostDocumentTest</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;d84945b2-716d-4cd9-94ad-d0a6aaa02982&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@Test
public void canPostAnyDocument() throws Exception {
    LocalDateTime now = LocalDateTime.now();
    Document createdDocument = postDocument.post(&#8220;username&#8221;, &#8220;text&#8221;);
    Document fetchedDocument = UseCaseContext.repository.getDocument(createdDocument.id);
    assertThat(fetchedDocument.username).isEqualTo(&#8220;username&#8221;);
    assertThat(fetchedDocument.text).isEqualTo(&#8220;text&#8221;);
    assertThat(fetchedDocument.id).isEqualTo(createdDocument.id);
    assertThat(fetchedDocument.dateTime).isEqualTo(createdDocument.dateTime);
}</code></pre></div><h2>Real-life example: eShop - placing orders</h2><p>Now let&#8217;s illustrate Uncle Bob&#8217;s approach to unit tests on a more realistic example - the eShop.</p><p>A customer places an order. The <code>PlaceOrder</code> use case takes a SKU and a quantity, looks up the product, checks stock, calculates the total order price, saves the order, notifies the customer, and returns the order number. It reaches the outside world through <strong>repository interfaces</strong> and <strong>gateway interfaces</strong>.</p><p>The domain holds two entities - <code>Order</code> and <code>Product</code> - plus value objects that own invariants a primitive can&#8217;t: <code>Quantity</code> (must be positive), <code>Money</code> (currency and arithmetic), <code>OrderNumber</code> (the format we generate). Those <strong>repository and gateway interfaces</strong> are part of the domain too - abstractions the use case owns and depends on, implemented out at the infrastructure edge.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rzfc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rzfc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png 424w, https://substackcdn.com/image/fetch/$s_!rzfc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png 848w, https://substackcdn.com/image/fetch/$s_!rzfc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png 1272w, https://substackcdn.com/image/fetch/$s_!rzfc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rzfc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png" width="1456" height="920" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/56400865-1465-41ff-9d6a-a6991928657d_1488x940.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:920,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85232,&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/208109632?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.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_!rzfc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png 424w, https://substackcdn.com/image/fetch/$s_!rzfc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png 848w, https://substackcdn.com/image/fetch/$s_!rzfc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.png 1272w, https://substackcdn.com/image/fetch/$s_!rzfc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56400865-1465-41ff-9d6a-a6991928657d_1488x940.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 aria-hidden="true" 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><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>Unit testing the PlaceOrder use case</h2><p>So when we adopt Clean Architecture, we write unit tests against the use case. The test class comes out like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;763e2c89-3522-4a99-aeb5-e85cbdaf78e5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">class PlaceOrderUnitTest {

    ...

    @BeforeEach
    void setUp() {
        orderRepository = new FakeOrderRepository();
        productGateway = new StubProductGateway();
        orderNumberGateway = new StubOrderNumberGateway();
        notificationGateway = new SpyNotificationGateway();

        placeOrder = new PlaceOrder(
            orderRepository, productGateway, orderNumberGateway, notificationGateway);
    }

    @Test
    void calculatesTheTotal() {
        productGateway.willReturn(new Product(&#8220;ABC&#8221;, Money.of(20), Quantity.of(10)));
        orderNumberGateway.willReturn(OrderNumber.of(&#8220;ORD-1001&#8221;));

        var request = new PlaceOrderRequest();
        request.setSku(&#8220;ABC&#8221;);
        request.setQuantity(4);

        var result = placeOrder.execute(request);

        assertThat(result.isSuccess()).isTrue();
        var addedOrder = orderRepository.getOrder(OrderNumber.of(&#8220;ORD-1001&#8221;));
        assertThat(addedOrder.getSku()).isEqualTo(&#8220;ABC&#8221;);
        assertThat(addedOrder.getTotalPrice()).isEqualTo(Money.of(80));
    }

    @Test
    void rejectsAnOrderThatExceedsStock() {
        productGateway.willReturn(new Product(&#8220;ABC&#8221;, Money.of(20), Quantity.of(10)));

        var request = new PlaceOrderRequest();
        request.setSku(&#8220;ABC&#8221;);
        request.setQuantity(15);

        var result = placeOrder.execute(request);

        assertThat(result.isSuccess()).isFalse();
        assertThat(orderRepository.isEmpty()).isTrue();
    }

    ...
}</code></pre></div><h3>&#128683;Maintenance nightmare - refactoring the domain breaks unit tests</h3><p>The tests are green. 100% code coverage, 100% mutation coverage.</p><p>But then, there comes a day when we need to refactor the domain. That&#8217;s when the nightmare starts...</p>
      <p>
          <a href="https://journal.optivem.com/p/fragile-unit-tests-in-clean-architecture">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA["Clean Architecture Is Overhead."]]></title><description><![CDATA[Stop counting lines of code]]></description><link>https://journal.optivem.com/p/clean-architecture-is-overhead</link><guid isPermaLink="false">https://journal.optivem.com/p/clean-architecture-is-overhead</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 28 Jul 2026 06:01:50 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8e607007-adb9-4a59-9711-1c4355f9ac77_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>&#8220;Clean Architecture is overkill.&#8221;</p><p>I&#8217;ve heard that a lot of times.</p><p>Too many interfaces. Too much abstraction. Too much indirection.</p><p>And some people go further and argue it makes the system harder to maintain, harder to understand.</p><h2>The overhead is real</h2><p>Compared to a plain CRUD application, yes, there are more interfaces. Specifically for anything that is infrastructure: databases, the file system, external REST APIs.</p><p>Instead of directly opening an HTTP client connection, or directly opening a database connection, or directly working with the ORM, you put an interface in between. Your use cases depend on those interfaces, i.e. abstractions of infrastructure, rather than directly depending on the infrastructure details.</p><p>And as a developer you have more files to look at, because when you open your use case class you&#8217;re not going to see the direct call to the HTTP client.</p><p>That&#8217;s overhead. I&#8217;m not going to pretend it isn&#8217;t.</p><h2>But it&#8217;s nothing compared to the size of the codebase</h2><p>For midsize and larger projects, that overhead is nothing compared to the size of the codebase.</p><p>It&#8217;s like someone arguing: let&#8217;s not use Java/C#, let&#8217;s use C/C++ for everything, because C and C++ are faster. Microseconds, milliseconds, whatever the number is. </p><p>And yet developers still use Java and .NET anyway, because the trade-off is worth what they get back.</p><p>Same thing here.</p><p><strong>A handful of extra interface files is not the argument they think it is</strong>.</p><h2>The harder one: domain entities vs ORM entities</h2><p>The complaint that&#8217;s genuinely harder to argue against is this one: you have your domain entities and you have your ORM entities, so now you&#8217;re doing double the work and maintaining double the code.</p><p>Initially, on a really small project, I can see that argument being hard to fight. At that point the domain entities and the ORM entities are most likely one-to-one identical. So yes, it does feel like duplication. It looks like you typed the same class twice for no reason.</p><p>But what happens, quite often, is that enterprise projects <strong>grow in complexity</strong>. And  that&#8217;s exactly when being coupled to the database hurts you.</p><p>The one-to-one mapping stops being one-to-one, and the class they were calling duplication turns out to be the reason you can change the business logic without being <strong>trapped by your database structure</strong>.</p><h2>Stop counting lines of code</h2><p>Instead of asking: &#8220;How many extra files does Clean Architecture create?&#8221; or &#8220;How many extra lines of code does this add?&#8221;&#8230;</p><p>Think about <strong>how hard it is to understand the code</strong>.</p><p>Separation of concerns means separating business logic from infrastructure so that it&#8217;s easier for our brains.</p><p>When you&#8217;re thinking about business requirements, you don&#8217;t want to also be thinking about what the external REST API DTO looks like. Later when you shift to integrating with external systems, that&#8217;s when you think about the nitty-gritty of the I/O and the DTOs. Those are <strong>two separate concerns</strong>. You go into one of them at a time.</p><p>That&#8217;s less effort to understand the code. That&#8217;s the payoff, and it doesn&#8217;t show up anywhere in a line count.</p><p>So next time someone tells you Clean Architecture is overhead, don&#8217;t argue about the number of files. Ask them how many things they have to hold in their head to change one business rule.</p><h2>&#9889;Clean Architecture in practice</h2><p>How do you separate business logic from infrastructure?<br>How to decouple the domain from the ORM?<br>When is an abstraction useful&#8212;and when is it over-engineering?</p><p>Join the live training session:</p><p><span>&#128467; </span><strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong><span> (Wed Aug 26)</span></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942;Register now&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/live_events/clean-architecture-for-backend-developers"><span>&#127942;Register now</span></a></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Clean Architecture vs Vertical Slice Architecture]]></title><description><![CDATA[&#8220;Why should I jump between ten different files just to understand one feature? Put everything for that feature in one place.&#8221;]]></description><link>https://journal.optivem.com/p/clean-architecture-vs-vertical-slice-architecture</link><guid isPermaLink="false">https://journal.optivem.com/p/clean-architecture-vs-vertical-slice-architecture</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 24 Jul 2026 14:17:10 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/044b3992-ecc8-49da-990b-1b7dd0a6a646_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>Every few months someone declares that Clean Architecture is dead.</p><p>The new answer?</p><p>Vertical Slice Architecture.</p><p>Usually it goes something like this:</p><blockquote><p>&#8220;Why should I jump between ten different files just to understand one feature? Put everything for that feature in one place.&#8221;</p></blockquote><p>Instead of splitting code into controllers, use cases, domain objects and repositories...</p><p>...put everything for <code>PlaceOrder</code> in one place.</p><p>One handler.</p><p>One folder.</p><p>One feature.</p><h2>"Everything is in one place"</h2><p>This is the promise of Vertical Slice Architecture.</p><pre><code><code>PlaceOrder
&#9500;&#9472;&#9472; HTTP Request Validation
&#9500;&#9472;&#9472; Inventory Rules
&#9500;&#9472;&#9472; Discount Rules
&#9500;&#9472;&#9472; Price Calculation
&#9500;&#9472;&#9472; SQL Query
&#9500;&#9472;&#9472; ORM Mapping
&#9500;&#9472;&#9472; SAP Integration
&#9492;&#9472;&#9472; HTTP Response Mapping</code></code></pre><p>Finance wants to change the discount rules.</p><p>Which parts are relevant?</p><pre><code><code>PlaceOrder
&#9500;&#9472;&#9472; HTTP Request Validation
&#9500;&#9472;&#9472; Inventory Rules
&#9500;&#9472;&#9472; Discount Rules &#11088;
&#9500;&#9472;&#9472; Price Calculation
&#9500;&#9472;&#9472; SQL Query
&#9500;&#9472;&#9472; ORM Mapping
&#9500;&#9472;&#9472; SAP Integration
&#9492;&#9472;&#9472; HTTP Response Mapping</code></code></pre><p>Only one item matters. </p><p>The rest is noise.</p><p>But to find it, you have to going through request validation, persistence, mappings and integrations that have <strong>nothing to do with discount rules</strong>.</p><p>That&#8217;s the mental overhead.</p><p>Because the business logic is <strong>mixed in</strong> with everything else.</p><h2>Don&#8217;t mix different concerns</h2><p>With Clean Architecture:</p><pre><code><code>Presentation Layer
&#9500;&#9472;&#9472; REST API Controllers
&#9500;&#9472;&#9472; HTTP Request Validation
&#9492;&#9472;&#9472; HTTP Response Mapping

Application Layer
&#9500;&#9472;&#9472; Place Order Use Case
&#9492;&#9472;&#9472; Cancel Order Use Case

Domain Layer
&#9500;&#9472;&#9472; Inventory Rules
&#9500;&#9472;&#9472; Discount Rules
&#9492;&#9472;&#9472; Price Calculation

Infrastructure Layer
&#9500;&#9472;&#9472; SQL Query
&#9500;&#9472;&#9472; ORM Mapping
&#9492;&#9472;&#9472; SAP Integration</code></code></pre><p>There&#8217;s more files, but&#8230;</p><p>Finance wants to change the discount rules.</p><p>You <strong>immediately know where to look.</strong></p><pre><code><code>Domain Layer
&#9500;&#9472;&#9472; Inventory Check
&#9500;&#9472;&#9472; Discount Rules &#11088;
&#9492;&#9472;&#9472; Price Calculation</code></code></pre><p>Done.</p><p>You don't have to mentally filter out SQL, HTTP, ORM mappings and SAP integration first.</p><p>Because the business logic is <strong>isolated</strong>.</p><h2>The context you DON&#8217;T need</h2><h3>&#10060; Vertical Slice</h3><p>&#128269;&#65038; <strong>Find discount rule</strong></p><ul><li><p>Open PlaceOrder</p></li><li><p>Scroll past request validation</p></li><li><p>Scroll past database code</p></li><li><p>Scroll past SAP mapping</p></li><li><p>Finally change discount rule</p></li></ul><h3>&#9989; Clean Architecture</h3><p>&#128269;&#65038; <strong>Find discount rule</strong></p><ul><li><p>Open Domain Layer</p></li><li><p>Change discount rule</p></li></ul><h1>&#128161;Real-life Example: Ordering System</h1><p>Finance doesn't just ask for one discount rule.</p><p>The <strong>rules start growing</strong>:</p><ul><li><p>Premium customers get 10% off</p></li><li><p>Orders above &#8364;1,000 get another 5% discount</p></li><li><p>Some products are excluded</p></li><li><p>Discounts cannot be combined</p></li><li><p>Regional promotions override standard discounts</p></li></ul><p>Now you need to change the discount calculation.</p><h2>&#10060; Vertical Slice Architecture</h2>
      <p>
          <a href="https://journal.optivem.com/p/clean-architecture-vs-vertical-slice-architecture">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Every Developer Is Already an Architect]]></title><description><![CDATA[Most developers think architecture is someone else&#8217;s job.]]></description><link>https://journal.optivem.com/p/every-developer-is-already-an-architect</link><guid isPermaLink="false">https://journal.optivem.com/p/every-developer-is-already-an-architect</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 21 Jul 2026 13:02:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cc9ff4ab-e13d-4502-a079-a8f662c55d54_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 developers think architecture is someone else&#8217;s job.</p><p>It&#8217;s what the architects do.</p><p>Or the tech leads.</p><p>Or the people who spend their days drawing boxes and arrows.</p><p>It&#8217;s not on your job description, so it&#8217;s not your problem.</p><p>Except it is.</p><p>By the time you&#8217;ve been building software for a few years, <strong>you&#8217;re already making architectural decisions.</strong></p><p>You just don&#8217;t call them that.</p><h2>A year later it's called "The Architecture"</h2><p>You&#8217;re implementing a new feature. An order has to notify the warehouse once it&#8217;s paid.</p><p>You need to decide where that notification comes from.</p><p>Do you call the warehouse API straight from the payment handler, because it&#8217;s two lines and the ticket is due Thursday?</p><p>Do you publish an event instead, and let the warehouse subscribe to it?</p><p>Do you put the rule &#8212; <em>an order notifies the warehouse once it&#8217;s paid</em> &#8212; inside <code>Order</code>, or in whichever service happens to be holding it?</p><p>Do you reuse the HTTP client that&#8217;s already wired up in the payment module, or give the warehouse call its own?</p><p>None of these decisions feel like &#8220;architecture.&#8221;</p><p>They seem to be just implementation details.</p><p><strong>Until a year later.</strong></p><p>A year later, a new developer joins and asks why payment knows about the warehouse. Why the notification rule lives in a service instead of in the order. Why there are four HTTP clients configured against the same host.</p><p>Nobody has an answer. The person who made those calls has moved on, or doesn&#8217;t remember, or never thought about it much...</p><p>But the answers have hardened. The direct call became the pattern everyone copied. The rule in the service became the reason <code>Order</code> can&#8217;t be tested without a database. The four clients became the reason a timeout change takes a day.</p><p><strong>That&#8217;s what the team now calls &#8220;the architecture.&#8221;</strong></p><p>Nobody designed it. It accumulated.</p><h2>The line between developer and architect isn't a promotion</h2><p>Architecture isn&#8217;t something you do before writing code.</p><p>It&#8217;s something you do every time you write code.</p><p>So the difference between a developer and an architect isn&#8217;t the title, and it isn&#8217;t permission from someone above you. It&#8217;s whether the decision got made on purpose.</p><p>Sometimes the direct call really is the right answer.</p><p>Picture two developers who both write it. The first one weighed it: the warehouse call is rare, a failed notification is recoverable, an event bus would cost more than it saves right now. The second one wrote it because it was the shortest path on a Thursday.</p><p>Read the code today and you can&#8217;t tell them apart. <strong>It&#8217;s the same two lines.</strong></p><p>The difference shows up the day the assumption breaks &#8212; when the call stops being rare, or a lost notification starts costing real money.</p><p>The first developer knows exactly what they traded away, so they recognize the moment the trade stops paying.</p><p>The second left nothing behind to revisit, so nobody notices. The line just gets copied into the next three features, because by then it looks like a decision somebody made.</p><p>Next time you reach for the shortest path, ask the architect&#8217;s question: <strong>what does this make cheap later, and what does it make expensive?</strong></p><p>You&#8217;re already making the decision.</p><p>Make it deliberately.</p><h2>&#9889;Clean Architecture in practice</h2><p>Which class should own a business rule?<br>What&#8217;s allowed to depend on what?<br>Which calls to the outside world you should be able to swap for a fake when you test?</p><p>Nobody tells you that along with the title.</p><p>Join the live training session:</p><p>&#128467; <strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong> (Wed Aug 26)</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942;Register now&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/live_events/clean-architecture-for-backend-developers"><span>&#127942;Register now</span></a></p>]]></content:encoded></item><item><title><![CDATA[TDD - Stop Mocking JPA: You're Testing the Wrong Thing]]></title><description><![CDATA[Code Example]]></description><link>https://journal.optivem.com/p/tdd-stop-mocking-jpa-youre-testing-wrong-thing</link><guid isPermaLink="false">https://journal.optivem.com/p/tdd-stop-mocking-jpa-youre-testing-wrong-thing</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 17 Jul 2026 06:01:26 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1c9112a7-5683-4773-a717-4d5de49ab6b7_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>One of the biggest mistakes I see in Spring applications is developers mocking JPA.</p><p>When tests become slow, they replace the database with mocked <code>JpaRepository</code> interfaces.</p><p>The tests are faster.</p><p>But the architecture is now coupled to the ORM.</p><h2>Why Mocking JPA Is a Design Smell</h2><p>Developers mock JPA because they want:</p><ul><li><p>fast tests</p></li><li><p>no database</p></li><li><p>isolated business logic</p></li></ul><p>Those are good goals.</p><p>Mocking JPA isn&#8217;t.</p><p>Why?</p><ul><li><p>You&#8217;re mocking a framework you don&#8217;t own</p></li><li><p>Your tests depend on Spring Data instead of your own abstractions</p></li><li><p>Changing persistence forces you to change unit tests</p></li><li><p>Business logic stays coupled to the ORM</p></li></ul><p>The mock removes the database.</p><p>It doesn&#8217;t remove the dependency on the ORM.</p><h2>The Real Problem</h2><p>One team I worked with couldn&#8217;t upgrade their ORM for months because of a change to how inheritance was mapped.</p><p>The application wasn&#8217;t the problem.</p><p>The business logic wasn&#8217;t the problem.</p><p>The problem was that the ORM had leaked into the application layer and the tests.</p><p>A change in persistence rippled through the entire codebase.</p><p><strong>A concrete example:</strong></p><ul><li><p>In .NET, EF Core's Table-Per-Hierarchy inheritance auto-adds a <code>Discriminator</code> column, and its behavior has shifted across major versions &#8212; nullability, length, how it's configured and queried.</p></li><li><p>An upgrade changes the discriminator, and that change ripples into every piece of code that touches those entities.</p></li><li><p>JPA has the same trap: single-table inheritance auto-creates a <code>DTYPE</code> discriminator column, so a change to the mapping reaches straight into the application layer and its tests.</p></li></ul><h1>&#128161; Code Example</h1>
      <p>
          <a href="https://journal.optivem.com/p/tdd-stop-mocking-jpa-youre-testing-wrong-thing">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Broken Systems Don't Need More Developers]]></title><description><![CDATA[If four developers aren&#8217;t delivering fast enough, why not make it eight?]]></description><link>https://journal.optivem.com/p/you-cant-fix-a-broken-system-by-adding-more-developers</link><guid isPermaLink="false">https://journal.optivem.com/p/you-cant-fix-a-broken-system-by-adding-more-developers</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 14 Jul 2026 09:12:34 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c9bf9b6c-687b-4e33-b242-70b1100eeeb1_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>Manager:</p><blockquote><p>"We're behind schedule! We need more developers."</p></blockquote><p>Senior developers don&#8217;t think:</p><blockquote><p>"Great, we'll finish sooner."</p></blockquote><p>Instead, it's more like:</p><blockquote><p>&#8220;We&#8217;re about to spend the next month onboarding.&#8221;</p></blockquote><p>Because they know what comes next&#8230;</p><ul><li><p>Onboarding</p></li><li><p>Interruptions</p></li><li><p>Explaining codebase</p></li><li><p>More meetings</p></li><li><p>More PR reviews</p></li><li><p>More merge conflicts</p></li></ul><h2>The Rules Nobody Wrote Down</h2><p>The system is a tangled mess of dependencies.</p><p>But after working on it for years, experienced developers have <strong>learned to navigate the mess</strong>.</p><p>They know:</p><ul><li><p>&#8220;Don&#8217;t touch that class.&#8221;</p></li><li><p>&#8220;Only Mike understands billing.&#8221;</p></li><li><p>&#8220;Changing this always breaks reporting.&#8221;</p></li><li><p>&#8220;That test is flaky, just rerun it.&#8221;</p></li></ul><p>They&#8217;ve built a mental map of the codebase.</p><p><strong>A new developer hasn&#8217;t.</strong></p><p>Every feature starts with: &#8220;Who knows this part of the system?&#8221;</p><p>They need someone to explain the codebase, the architecture, and all the hidden pitfalls nobody documented.</p><p>More detailed code reviews.</p><p>More meetings.</p><h2>More Developers. Less Progress.</h2><p>The bottleneck isn&#8217;t the number of developers.</p><p>It&#8217;s the architecture.</p><p>When the system is tightly coupled, <strong>every change collides with another change.</strong></p><p>Adding more people doesn't remove the bottleneck.</p><p>It just makes it worse.</p><div><hr></div><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><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;Clean Architecture in Practice &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers"><span>Clean Architecture in Practice &#8594;</span></a></p>]]></content:encoded></item><item><title><![CDATA[Clean Architecture Mistake: ORM ≠ Domain]]></title><description><![CDATA[Do NOT confuse ORM entities with domain entities.]]></description><link>https://journal.optivem.com/p/clean-architecture-mistake-orm-domain</link><guid isPermaLink="false">https://journal.optivem.com/p/clean-architecture-mistake-orm-domain</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 09 Jul 2026 06:02:30 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d3e29bda-7d3c-42c3-8b02-21b4c8c978d0_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>Your ORM has entities.</p><p>Clean Architecture talks about entities.</p><p>They sound like the same thing.</p><p>They&#8217;re not.</p><h2>&#10060; Your ORM Becomes Your Domain</h2><p>You create an ORM entity (JPA entity) because you need to store orders.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;88aa993c-13cb-4128-8a41-e2ab127df434&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@Entity
class Order {
    @Id
    private Long id;

    private OrderStatus status;
    private int quantity;
    private BigDecimal unitPrice;
}</code></pre></div><p>And an ORM repository (JPA repository) to save and load it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;dfa1068a-99a5-49fa-9d99-2d84818374ff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">interface OrderJpaRepository extends JpaRepository&lt;Order, Long&gt; {
    // findById and save come out-of-the-box:
    // Optional&lt;Order&gt; findById(Long id);
    // Order save(Order order);
}</code></pre></div><p>So far, so good.</p><p>Its job is persistence.</p><p><span>Then you need to place an order &#8212; and you expect an order to be in a valid state before it is saved. So you give </span><code>Order</code><span> a constructor that refuses to build an invalid order &#8212; e.g. one with a non-positive quantity or a null unit price:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;2bc989b2-2f18-4eae-9844-bd79e39900a3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">Order(int quantity, BigDecimal unitPrice) {
    if (quantity &lt;= 0) {
        throw new RuntimeException("quantity must be positive");
    }

    if (unitPrice == null) {
        throw new RuntimeException("unit price is required");
    }

    this.status = OrderStatus.PLACED;
    this.quantity = quantity;
    this.unitPrice = unitPrice;
}</code></pre></div><p>Now an <code>Order</code> cannot exist in a broken state.</p><p>Your use case builds one through it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;093d091d-d908-44f9-a9ba-14f4bd7e6d00&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">class PlaceOrder {

    void execute(int quantity, BigDecimal unitPrice) {
        var order = new Order(quantity, unitPrice);
        orderRepository.save(order);
    }
}</code></pre></div><p>And your use case depends on it.</p><p>Your ORM entity has become your domain entity.</p><p>It looks fine. It compiles. It runs.</p><p>Except &#8212; declaring that constructor removed Java&#8217;s free no-arg one. And JPA cannot live without one.</p><p>That&#8217;s worse than a plain compile error. It compiles, it starts up, and it even saves correctly &#8212; then breaks the first time someone reads the order back. Nothing warns you until then.</p><p>So you&#8217;re forced to add it back:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;38e5a664-86e3-42f6-ac42-6c624af01000&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">protected Order() {}   // required by JPA &#8212; status null, quantity 0, unitPrice null</code></pre></div><p>You wrote a constructor to reject invalid orders.</p><p>JPA makes you add a no-arg one next to it &#8212; one that builds an order with a null status, zero quantity, no price.</p><p>Marking it <code>protected</code> doesn&#8217;t help. When Hibernate loads a row, it ignores every access modifier by using reflection: it invokes that no-arg constructor, then sets the fields one by one.</p><p>Your validating constructor never runs. Your checks never execute.</p><p>An order no business would accept &#8212; and Hibernate builds one on every load.</p><p>Here is the problem.</p><p>An object can only enforce its invariants in one place: its <strong>constructor</strong>. JPA bypasses it &#8212; it forces a no-arg constructor, then sets the fields by reflection, skipping your real constructor on every load.</p><p>So the real problem is not that &#8220;your domain is coupled.&#8221;</p><p>It&#8217;s this:</p><p><strong>This entity cannot guarantee it's always valid (enforce business rules).</strong></p><p>Your validation only runs when you call the constructor yourself. JPA never calls it &#8212; on every load it constructs the object empty and sets the fields directly.</p><p>And every transition you validate later &#8212; cancel, ship, and so on &#8212; then runs on an object that was never validated in the first place.</p><p>The code compiles, it runs, and every invariant you thought you wrote is optional.</p><p>And it doesn&#8217;t stop at <code>Order</code>.</p><p>Use a <code>Money</code> value object instead of <code>BigDecimal</code> &#8212; immutable, always valid, exactly what DDD wants:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;f619f5cc-1462-4dd2-bff8-c18deaef63f2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">final class Money {
    private final BigDecimal amount;

    Money(BigDecimal amount) {
        if (amount == null) {
            throw new RuntimeException("amount is required");
        }

        this.amount = amount;
    }
}</code></pre></div><p><span>To persist it, JPA needs it as </span><code>@Embeddable</code><span>. And </span><code>@Embeddable</code><span> demands the same price: drop </span><code>final</code><span>, add a no-arg constructor.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;d049ea88-4152-4265-9ef5-0a4d3550ac94&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@Embeddable
class Money {                    // no longer final
    private BigDecimal amount;   // no longer final

    protected Money() {}         // required by JPA &#8212; amount null

    Money(BigDecimal amount) {
        if (amount == null) {
            throw new RuntimeException("amount is required");
        }

        this.amount = amount;
    }
}</code></pre></div><p>Your always-valid <code>Money</code> can now be built with a null amount too.</p><p>The trap isn&#8217;t contained to one class. It spreads to every value object you embed.</p><p>Notice what&#8217;s happening. You&#8217;re no longer modelling the domain &#8212; you&#8217;re shaping it to fit the ORM. You drop <code>final</code>, add constructors you&#8217;d never write, expose state you meant to hide &#8212; not because the business asked for it, but because the ORM expects it that way.</p><p>You end up violating the very principles the domain is supposed to protect, encapsulation first among them, just to keep the persistence framework happy.</p><p>That is <strong>not</strong> Clean Architecture.</p><div><hr></div><p>Want to avoid the ORM trap?</p><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><h2>&#9989;Domain &#8800; ORM</h2>
      <p>
          <a href="https://journal.optivem.com/p/clean-architecture-mistake-orm-domain">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Your Architecture Doesn't Rot Overnight]]></title><description><![CDATA[As deadlines become tighter, convenience starts winning.]]></description><link>https://journal.optivem.com/p/your-architecture-doesnt-rot-overnight</link><guid isPermaLink="false">https://journal.optivem.com/p/your-architecture-doesnt-rot-overnight</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 07 Jul 2026 06:00:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c5857b79-d8b2-4a92-bb8d-b34384b1752a_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>&#128075; </span><em><span>Hello, this is Valentina with the free edition of the Optivem Journal. I help Engineering Leaders &amp; Senior Software Developers apply </span><a href="https://journal.optivem.com/p/tdd-in-legacy-code-transformation">TDD in Legacy Code</a><span>.</span></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><h2>Every project starts simple</h2><p>Most enterprise applications begin with relatively simple requirements.</p><p>Creating records.</p><p>Updating data.</p><p>Calling external systems.</p><p>Displaying results.</p><p>At this stage, <strong>almost any architecture works</strong>. The codebase is small, everyone understands it, and adding new features is straightforward.</p><p>Then the project grows.</p><h2>&#8220;Just this once&#8221;</h2><p>A business rule needs data that&#8217;s already available in the repository.</p><p>Instead of passing it back to the use case, the rule is implemented directly in the repository.</p><p><strong>It&#8217;s only a few lines.</strong></p><p>No one wants to create another object or move data around.</p><p>The feature ships.</p><p>A few weeks later, another feature needs something similar.</p><p>The repository already knows about the data, so another business rule is added there.</p><p>Still reasonable.</p><p>Still working.</p><p>Still &#8220;just this once.&#8221;</p><h2>Convenience becomes the architecture</h2><p>As deadlines become tighter, convenience starts winning.</p><p>A controller performs a quick permission check because it&#8217;s only needed there.</p><p>A service calls another service because it avoids duplicating code.</p><p>An external API client starts making business decisions because it already has the response.</p><p>A repository calculates values because it has all the necessary information.</p><p><strong>None of this breaks the application.</strong></p><p>But it weakens the boundaries between business logic and infrastructure.</p><h2>The cost doesn&#8217;t appear immediately</h2><p>This is why architectural decay is so difficult to notice.</p><p>The application still works.</p><p>Tests still pass.</p><p>Deployments continue.</p><p>The problem appears months later.</p><p>A business rule needs changing.</p><p>Now the team isn&#8217;t sure where that rule actually lives.</p><p>Part of it is in a controller.</p><p>Part is inside a repository.</p><p>Another piece exists in an API adapter.</p><p>There&#8217;s similar logic somewhere else too&#8212;but no one knows whether it&#8217;s safe to change.</p><p>A feature that should have taken an hour becomes an afternoon of investigation.</p><p>Not because the rule is complicated.</p><p>Because the architecture is a <strong>Big Ball of Mud.</strong></p><p>You&#8217;re told: &#8220;Why is it taking so long? It&#8217;s just a simple requirement.&#8221;</p><h2>The trap is delaying</h2><p>Most teams recognize when the codebase is becoming harder to change.</p><p>They simply postpone doing anything about it.</p><p>&#8220;We&#8217;ll clean it up later.&#8221;</p><p>&#8220;We&#8217;ll refactor after this release.&#8221;</p><p>&#8220;We don&#8217;t have time right now.&#8221;</p><p>Meanwhile, every new feature adds another dependency, another shortcut, another place where business logic leaks into infrastructure.</p><p>Eventually, everyone agrees the system needs refactoring.<br>It&#8217;s just too difficult to start.</p><p>Join the live training session:</p><p><strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong></p><p><span>&#128467; Aug 26<br>&#9200; 5:00&#8211;6:30 PM (CEST)</span></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942;Register now&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/live_events/clean-architecture-for-backend-developers"><span>&#127942;Register now</span></a></p>]]></content:encoded></item><item><title><![CDATA[TDD in Legacy Code - Maintainable Component Tests - Backend]]></title><description><![CDATA[Many Backend Teams write unmaintainable Backend Component Tests - coupled to the Backend API and ERP. I'll show you how to refactor these brittle tests.]]></description><link>https://journal.optivem.com/p/maintainable-component-tests-in-legacy-code-backend</link><guid isPermaLink="false">https://journal.optivem.com/p/maintainable-component-tests-in-legacy-code-backend</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 02 Jul 2026 06:02:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/64035a71-683a-410a-9b9a-5d2279529b79_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>&#128197; Join me: </span><strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong><span> on Wed 26th Aug, 5:00 - 6:30 PM (CEST)</span></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942; Register now&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/live_events/clean-architecture-for-backend-developers"><span>&#127942; Register now</span></a></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 TDD in Legacy Code. This article is part of the <a href="https://journal.optivem.com/p/tdd-in-legacy-code-outline">TDD in Legacy Code</a> series. </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><h2>Backend Component Tests provide fast feedback</h2><p>We&#8217;ve seen in the previous article <a href="https://journal.optivem.com/p/component-tests-in-legacy-code-backend">Backend Component Tests in Legacy Code</a>, that Component Tests can provide us with fast feedback. The Backend Team can test the Backend in isolation, by stubbing out External Systems (such as the ERP).</p><h2>But they can be a maintenance nightmare!</h2><p>In <a href="https://journal.optivem.com/p/component-tests-in-legacy-code-backend">Backend Component Tests in Legacy Code</a>, we illustrated the &#8220;simplest&#8221; Backend Component Test.</p><p>The simplest way to write a Backend Component Test is to stub the External System inline (with WireMock), call the Backend API directly (with <code>WebTestClient</code>), and read the response by digging into raw JSON paths.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;1bdb7eb8-85cf-47a1-8d23-2a35567b29a0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@Test
void shouldCreateOrderWithTotalPrice() {
    // Arrange
    var productDto = new ProductDto(2.50);
    var productDtoJson = objectMapper.writeValueAsString(productDto);
    erpWireMockStub.stubFor(WireMock.get(&#8220;/products?sku=APPLE1001&#8221;)
        .willReturn(WireMock.aResponse()
            .withStatus(200)
            .withHeader(&#8220;Content-Type&#8221;, &#8220;application/json&#8221;)
            .withBody(productDtoJson)));

    var orderRequest = new OrderRequest(&#8220;APPLE1001&#8221;, 5);

    // Act &amp; Assert
    webTestClient.post()
        .uri(&#8220;/api/orders&#8221;)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(orderRequest)
        .exchange()
        .expectStatus().isCreated()
        .expectBody()
        .jsonPath(&#8220;$.totalPrice&#8221;).isEqualTo(12.5);
}</code></pre></div><p>The problem is that this test is coupled in three places at once: the <strong>ERP wire format</strong> (the WireMock stub), the <strong>Backend API endpoint</strong> (<code>webTestClient.post().uri("/api/orders")</code>), and the <strong>response </strong>(the raw <code>$.totalPrice</code> JSON path).</p><p>If the API endpoint changes, or the ERP&#8217;s wire format changes (a renamed field, a different status code), then many such tests may break, so we have to waste time fixing tests. The plumbing is also copy-pasted into every test.</p><h2>How to write maintainable Backend Component Tests?</h2><p>In this article, I&#8217;ll show you how to introduce layers of abstraction, i.e. Component Test Architecture, so that you spend much less time writing &amp; maintaining these tests.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4hma!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4hma!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png 424w, https://substackcdn.com/image/fetch/$s_!4hma!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png 848w, https://substackcdn.com/image/fetch/$s_!4hma!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png 1272w, https://substackcdn.com/image/fetch/$s_!4hma!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4hma!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png" width="1456" height="464" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:464,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:243475,&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/204517102?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.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_!4hma!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png 424w, https://substackcdn.com/image/fetch/$s_!4hma!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png 848w, https://substackcdn.com/image/fetch/$s_!4hma!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.png 1272w, https://substackcdn.com/image/fetch/$s_!4hma!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c053a5b-134b-4952-8b38-3ed64779c40d_3886x1239.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 aria-hidden="true" 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><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>Here are the steps to introduce Maintainable Backend Component Tests in Legacy Code. You&#8217;ll get tasks to implement in your GitHub Sandbox Project. &#11015;&#65039;&#11015;&#65039;&#11015;&#65039;</p>
      <p>
          <a href="https://journal.optivem.com/p/maintainable-component-tests-in-legacy-code-backend">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[You Didn't Become a Senior Dev to Firefight]]></title><description><![CDATA[The field took twenty minutes. Everything around it took three days.]]></description><link>https://journal.optivem.com/p/you-didnt-become-a-senior-dev-to-firefight</link><guid isPermaLink="false">https://journal.optivem.com/p/you-didnt-become-a-senior-dev-to-firefight</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 30 Jun 2026 06:02:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4c943ec9-688f-4372-88b2-3273e416b2f1_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>Think back to why you got into this.</p><p>Not the title or the pay bump. You wanted to be the person who shapes how the system is built instead of just closing the next ticket. </p><p>But how much of last month did you actually spend being that person? Or did it disappear into a three-day slog to make a small change &#8212; hoping it wouldn&#8217;t break something you couldn&#8217;t see?</p><h2>&#8220;Can you just add a field?&#8221;</h2><p>You know the one. A stakeholder wants one extra value on a form, and it lands on your desk &#8212; because you&#8217;re the one who gets handed the changes nobody else wants to touch. It&#8217;s a field. An afternoon, you figure.</p><p>Three days later you&#8217;re still in it.</p><p>The field touched a model. The model was wired straight into a service. The service was called from four places, two of which you didn&#8217;t know existed. There were no tests, so every change was a guess you couldn&#8217;t verify. And half of it was written by someone who left eighteen months ago, in a style you spent most of Tuesday just <em>reading</em> before you dared touch it.</p><p><strong>The field took twenty minutes.</strong></p><p><strong>Everything around it took three days.</strong></p><p>But&#8230; it didn&#8217;t feel like <em>work</em>. It felt like waste. Three days of picking through someone else&#8217;s tangle just to safely add a field &#8212; and not one minute of it went into anything that mattered, or anything that even held your interest.</p><p>You closed the ticket bored, drained, and quietly resentful that <em>this</em> is what your week had become.</p><h2>Little fixes get in the way of better architecture</h2><p>What makes it maddening is that you can <em>see</em> the work you&#8217;d rather be doing. You want to step back and redesign this &#8212; draw the boundaries that should&#8217;ve been there, untangle the core, build something that doesn&#8217;t fight you on every change. That&#8217;s the work that&#8217;s interesting. That&#8217;s the work that makes an impact.</p><p>But you never get to it, because there&#8217;s always one more little fix in the way, and the little fixes never stop.</p><p>And here&#8217;s what all that lost time costs you &#8212; the system you actually want to build:</p><ul><li><p>The clean, decoupled layers you know how to design &#8212; where a change lands in one place instead of rippling through five &#8212; stay tangled, because you only ever get time to patch, never to reshape.</p></li><li><p>The codebase that could be a pleasure to move through stays a maze you have to re-learn every time you open it.</p></li><li><p>The architecture you can already picture loses, every single sprint, to one more little fix you can&#8217;t say no to.</p></li></ul><p>You just notice, a year in, that you&#8217;re <strong>working as hard as ever, more bored than you&#8217;ve ever been, and no closer to the system you wanted to build</strong>.</p><h2>You&#8217;re stuck in a vicious cycle </h2><p>This isn&#8217;t bad luck, and it isn&#8217;t a talent problem. </p><p>The architecture is tightly coupled, so every change is slow and risky. Because every change is slow, there&#8217;s never a clear stretch of time to stop and fix the coupling &#8212; so you patch around it instead. And every patch wires one more thing to one more thing, which makes the <em>next</em> change even slower. </p><p><strong>Tightly coupled architecture.</strong> When everything reaches into everything else, a &#8220;small&#8221; change doesn&#8217;t stay small &#8212; it ripples across half the codebase, because half the codebase depends on the thing you touched.</p><p><strong>No tests, or tests you can&#8217;t trust.</strong> Without them, every change is a leap in the dark. You can&#8217;t move <em>boldly</em>, so you move <em>carefully</em> &#8212; and careful is slow.</p><p><strong>Unreadable code.</strong> Before you can change anything, you have to understand it. If understanding takes a day, every change loses a day to just figuring out what&#8217;s there before the real work even starts.</p><p>That&#8217;s why it doesn&#8217;t just stay annoying &#8212; it <em>compounds</em>. And the worst part is that you can <em>see</em> it happening: you&#8217;re being asked to add another floor to a house you know has no foundations. Every feature makes the structure taller and shakier, and you can feel that one ordinary change, on one ordinary day, is going to bring a piece of it down.</p><h2>Get the architecture right and everything else gets easier</h2><p>Here&#8217;s the reframe that changes how you spend your week: the <strong>architecture isn&#8217;t a chore for when there&#8217;s spare time</strong>, and it isn&#8217;t polish you bolt on at the end &#8212; it&#8217;s the one thing that everything else rides on.</p><p>The shape of the system &#8212; where the boundaries sit, what depends on what, how independent the core is &#8212; decides whether the next change lands in an afternoon or turns into another three-day dig.</p><p>And this is exactly where a senior developer makes their mark.</p><p>You&#8217;re the one teammates already ask <em>&#8220;where should this go?&#8221;</em> You&#8217;re the one who can introduce a boundary, decouple the layers so the core stops depending on the framework around it &#8212; and set the pattern the rest of the codebase follows.</p><h2>You know the architecture is painful</h2><p><strong>You didn&#8217;t become a senior developer to spend three days adding a field.</strong></p><p>But that&#8217;s what a lot of backend work becomes.</p><p>Not because the feature is hard.<br>Because the system is.</p><p>Join the live session:</p><p><strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong></p><p><span>&#128467; Aug 26<br>&#9200; 5:00&#8211;6:30 PM (CEST)</span></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942;Register now&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/live_events/clean-architecture-for-backend-developers"><span>&#127942;Register now</span></a></p><div><hr></div><p>And if you want to talk it through for <em>your</em> situation &#8212; not the textbook version &#8212; come join the <strong><a href="https://circle.optivem.com/">Optivem Circle Membership</a></strong></p>]]></content:encoded></item><item><title><![CDATA[Design your ATDD AI Workflow]]></title><description><![CDATA[Watch now | How to use AI in a reliable way with ATDD?]]></description><link>https://journal.optivem.com/p/design-your-atdd-ai-workflow</link><guid isPermaLink="false">https://journal.optivem.com/p/design-your-atdd-ai-workflow</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Thu, 25 Jun 2026 06:02:12 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/201267898/993e17de9df0d45645a8178a6b2e3cfa.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p><strong>&#128070;For non-paid subscribers, you can see a free preview above. The complete 1hr session is accessible to paid subscribers.</strong></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><p>When I first started using ATDD with AI, I did what most people would do.</p><p>I wrote a document describing the ATDD process, so that the agent could read that document. The AI was behaving unreliably, sometimes skipping steps in the process - e.g. implementing the code and testing simultaneously, rather than writing the test before the code. I also wasted so many tokens and ended up with an additional &gt; $1,000 bill.</p><p>Based on that lesson, I dug into token optimization, including splitting up that document. But still AI was behaving unreliably and I was spending a lot of tokens.</p><p>Then I realized the fundamental mistake that I was making&#8230;</p><p>That&#8217;s why in this live session, I shared my lessons learnt and my approach to practicing ATDD &amp; AI effectively.</p><ol><li><p>In the live session, I gave the big picture of my ATDD AI workflow design</p></li><li><p>I&#8217;m also <a href="https://leanpub.com/atdd-with-ai-agents">writing the book &#8220;ATDD with AI Agents&#8221; - join the waitlist</a>.</p></li><li><p>And, if you want to practice ATDD in your real life job, I&#8217;m opening enrollments for the ATDD Accelerator program. Limited spots. <a href="https://calendly.com/valentinajemuovic/atdd-accelerator">Book a call</a>.</p></li></ol>
      <p>
          <a href="https://journal.optivem.com/p/design-your-atdd-ai-workflow">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Biggest Bottleneck In Development Isn't Coding]]></title><description><![CDATA[Faster code without guardrails makes delivery slower]]></description><link>https://journal.optivem.com/p/the-biggest-bottleneck-in-development-isnt-coding</link><guid isPermaLink="false">https://journal.optivem.com/p/the-biggest-bottleneck-in-development-isnt-coding</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Mon, 22 Jun 2026 06:01:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e3fba412-3e24-4c2d-bfc2-657f77d0fbaf_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>Ask a manager what a developer does all day and you&#8217;ll get one answer:</p><blockquote><p>They code.</p></blockquote><p>The manager pictures eight hours of typing, so when a &#8220;two-day&#8221; change takes a week, the gap looks like slack.</p><p>But the picture is wrong.</p><h2>Where the time actually goes</h2><p>Coding is just a small slice.</p><p>The rest is:</p><ul><li><p><strong>Testing</strong> &#8212; the developer tests it, then QA tests it again</p></li><li><p><strong>Deployment</strong> &#8212; still a manual procedure in most organizations, repeated for every environment</p></li><li><p><strong>Code review</strong> &#8212; PRs that sit, get comments, get revised, get re-reviewed</p></li><li><p><strong>Requirements</strong> &#8212; scattered across tickets, email, Slack, and three half-remembered conversations</p></li><li><p><strong>Meetings</strong> &#8212; the standing tax on every working day</p></li><li><p><strong>Rework</strong> &#8212; the change that comes back because the developer, QA and the PO each understood the requirement differently</p></li></ul><p>The frustrating part, if you&#8217;re the one doing the work, is that the thing you actually wanted to do &#8212; design and code &#8212; is the sliver you fight to protect.</p><p>Everything else eats the day.</p><h2>AI optimized the wrong thing</h2><p>Because if coding was never the bottleneck&#8230;</p><p>You&#8217;ve sped up the smallest slice and left testing, deployment, review, requirements, and rework exactly where they were.</p><h2>Faster code without guardrails makes delivery slower</h2><p>More code, faster &#8212; without guardrails (automated testing, automated deployment) &#8212; means more regression bugs slip through.</p><p>Those regression bugs land on the same manual QA who were already a bottleneck, now buried under even more changes to verify by hand.</p><p>And this is happening alongside layoffs justified by &#8220;AI makes us faster.&#8221;</p><h2>The real lever is the whole pipeline</h2><p>If most of the delivery time is <strong>non-coding work</strong>, build the pipeline so the <strong>non-coding steps run automatically.</strong></p><p>Testing and deployment are often executed by someone following manual procedures.<br>But it shouldn&#8217;t be.</p><p>Unit testing should be run on every change. Deployment and system testing should be run on a regular interval. In this way, we get faster feedback. We refuse to promote anything that fails.</p><h2>See It in Practice</h2><p>This isn&#8217;t a course you watch alone at 2x speed and forget by Friday.</p><p><strong>What you&#8217;ll learn:</strong></p><p><strong>1. Pipeline Architecture.</strong><span> </span>Stages &#8212; Commit, Acceptance, Release &#8212; what belongs in each, and why testing the same build makes deployments predictable</p><p><strong>2. AI, TDD &amp; ATDD in the Pipeline.</strong><span> </span>Automated tests &#8212; unit, narrow integration, component, contract, smoke, acceptance, external system contract, e2e &#8212; and how AI &amp; ATDD/TDD workflows fit within the Pipeline</p><p><strong>3. Apply it with your team.</strong><span> </span>How to present this architecture to your team, get buy-in, and start the move away from firefighting</p><p><strong>When:</strong><span> June 24&#8211;25, 2026 | 5-7 PM CET</span><br><strong>Where:</strong><span> Live on Zoom</span><br><strong>Duration:</strong><span> 4 hours (2 sessions x 2 hours)</span></p><p><span>&#128187; </span><strong>Who it&#8217;s for:</strong><span> Senior Engineers and Tech Leads who are stuck with stressful releases and ready to do something about it</span></p><p><span>&#128640; </span><strong>Register:<span> </span><a href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop">Pipelines Workshop</a><br><span>Get &#8364;100 off with code</span></strong><span> </span><strong>DISCOUNT_100</strong></p><p>Want to stop stressful releases, late-night fixes, and broken deployments? I&#8217;m running a hands-on <strong><a href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop">Pipelines Workshop</a></strong> on June 24-25 (4 hours).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MLsU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!MLsU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!MLsU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!MLsU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MLsU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:199165,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-06-pipeline-workshop&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://journal.optivem.com/i/202562542?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.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_!MLsU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!MLsU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!MLsU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!MLsU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F174bf3a1-c8a0-4450-941d-0482d7b7ab30_1280x720.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 aria-hidden="true" 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><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 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 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 the workshop &#8594;</span></a></p><p><span data-color="rgb(54, 55, 55)" style="color: rgb(54, 55, 55);">Limited spots. Register now with - </span><strong>&#8364;100 off with code DISCOUNT_100</strong></p><p></p>]]></content:encoded></item><item><title><![CDATA[One Build, One Deploy Script, Many Environments]]></title><description><![CDATA[What we TESTED is what we SHIP]]></description><link>https://journal.optivem.com/p/one-build-one-deploy-script-many-environments</link><guid isPermaLink="false">https://journal.optivem.com/p/one-build-one-deploy-script-many-environments</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 19 Jun 2026 07:56:38 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0349d3b5-2e15-453d-9f72-5a1370d94940_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;Should we build once, or build per environment?&#8221;</p><p>&#8220;Should production have its own deploy workflow?&#8221;</p></blockquote><p>These are the same question.</p><p>The first is about running a <strong>fresh </strong><code>docker build</code><strong> per environment</strong> &#8212; one build for QA, another for production, &#8220;because production needs different config baked in.&#8221;</p><p>The second is about writing <strong>a separate deploy workflow per environment</strong> &#8212; a <code>deploy-qa.yml</code>, a <code>deploy-production.yml</code>, each with its own steps and copy-pasted-then-edited logic, drifting apart commit by commit.</p><p>Both come from the same place: <em>this environment is special, so it needs its own thing.</em></p><p>A pipeline&#8217;s entire job is to make &#8220;what we tested&#8221; and &#8220;what we shipped&#8221; the same. Every per-environment fork is a place where they quietly stop being the same.</p><h2>Build once: no second build</h2><p>The build is created in the Commit Stage. Every stage after that uses the <em>same build</em> &#8212; it gets tagged and deployed, but never rebuilt.</p><p>The temptation to rebuild happens because of configuration: &#8220;production needs the production API URL,&#8221; &#8220;QA needs the QA feature flags.&#8221;</p><p>So a second <code>docker build</code> happens with different build args, and now production is running a build that <strong>no test ever ran against</strong>. The green checkmark from QA is for a different build than the one customers actually get.</p><p>The build has no environment. Configuration is injected at deploy time &#8212; environment variables, mounted config, secrets from the environment &#8212; into the <em>same build</em>. That&#8217;s what allows the same build to move from Acceptance to QA to Production unchanged.</p><div><hr></div><p>&#9889;<strong>Want to stop stressful releases, late-night fixes, and broken deployments?</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 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-06-pipeline-workshop"><span>Join the workshop &#8594;</span></a></p><p><span data-color="rgb(54, 55, 55)" style="color: rgb(54, 55, 55);">Limited spots. Register now - </span><strong>100 EUR off with code DISCOUNT_100</strong></p><div><hr></div><h2>Deploy once: same script, different environment</h2><p>Here&#8217;s the same deploy step running in three different environments.</p><p><strong>Acceptance:</strong></p><pre><code>- <span data-color="rgb(5, 80, 174)" style="color: rgb(5, 80, 174);">name</span>: <span data-color="rgb(10, 48, 105)" style="color: rgb(10, 48, 105);">Deploy</span>
  <span data-color="rgb(5, 80, 174)" style="color: rgb(5, 80, 174);">uses</span>: <span data-color="rgb(10, 48, 105)" style="color: rgb(10, 48, 105);">acme/actions/deploy@v1</span>
  <span data-color="rgb(5, 80, 174)" style="color: rgb(5, 80, 174);">with</span>:
    <span data-color="rgb(5, 80, 174)" style="color: rgb(5, 80, 174);">environment</span>: <span data-color="rgb(10, 48, 105)" style="color: rgb(10, 48, 105);">acceptance</span>
    <span data-color="rgb(5, 80, 174)" style="color: rgb(5, 80, 174);">version</span>: <span data-color="rgb(10, 48, 105)" style="color: rgb(10, 48, 105);">${{ inputs.version }}     </span><span data-color="rgb(89, 99, 110)" style="color: rgb(89, 99, 110);"># the system version, e.g. v2.5.0-rc.3</span>
    <span data-color="rgb(5, 80, 174)" style="color: rgb(5, 80, 174);">image-urls</span>: <span data-color="rgb(10, 48, 105)" style="color: rgb(10, 48, 105);">|</span>
<span data-color="rgb(10, 48, 105)" style="color: rgb(10, 48, 105);">      ghcr.io/acme/shop/frontend</span>
<span data-color="rgb(10, 48, 105)" style="color: rgb(10, 48, 105);">      ghcr.io/acme/shop/backend</span></code></pre>
      <p>
          <a href="https://journal.optivem.com/p/one-build-one-deploy-script-many-environments">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Feature Branching Is NOT a Strategy]]></title><description><![CDATA[If your branches outlive the day &#8212; you have a backlog of merge conflicts.]]></description><link>https://journal.optivem.com/p/feature-branching-is-not-a-strategy</link><guid isPermaLink="false">https://journal.optivem.com/p/feature-branching-is-not-a-strategy</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 16 Jun 2026 04:54:23 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d5867c6a-91cc-435d-b54e-96c2f221d822_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>Every ticket gets a branch.</p><p>Nobody decided this. There was no meeting, no architecture review, no trade-off weighed. It&#8217;s just the reflex &#8212; the default Git workflow everyone inherited the day they learned <code>git checkout -b</code>. New ticket, new branch. Open it, work on it for as long as the work takes, merge it when it&#8217;s done.</p><p>And it is quietly the reason &#8220;we do CI&#8221; is a lie.</p><p>A branch that lives for a week is not CI. It&#8217;s a private fork.</p><p>And a private fork is the <em>opposite</em> of continuous integration &#8212; you are NOT integrating continuously. You are integrating at the end.</p><h2>A Long-Lived Branch Is a Private Fork</h2><p>Here is what a feature branch actually is: a copy of the codebase that diverges from everyone else&#8217;s copy, a little more, every single day it stays open.</p><p>On day one, the difference is small &#8212; your branch and main differ by your few lines. </p><p>By day five, main has changed &#8212; three other developers merged their own week-long branches. Your feature branch has changed too, and you only find out how far apart they are when you try to merge.</p><p>The conflicts aren&#8217;t just textual. Two developers refactored the same function for different reasons. A method you are calling got deleted. An interface was modified.</p><p>The merge you keep deferring is getting more expensive every day you don&#8217;t do it.</p><p>This is the trap: merging hurts, so the team merges <em>less</em> often, so branches live <em>longer</em>, so the next merge hurts <em>more</em>. &#8220;I&#8217;ll keep my work isolated until it&#8217;s solid&#8221; &#8212; is the exact thing causing the pain.</p><h2>Trunk-Based Development</h2><p>Trunk-based development is the decision to stop deferring.</p><p>It&#8217;s not &#8220;no branches.&#8221; That&#8217;s the strawman people use to dismiss it. It&#8217;s <em>no long-lived branches</em></p><p>&#10004; Everyone integrates to main at least once a day.</p><p>&#10004; Branches are fine &#8212; as long as they live hours, not weeks.</p><p>&#10004; The trunk is the single shared integration point. There is no &#8220;at the end&#8221; &#8212; only now.</p><p>&#10060; No branch that survives the sprint.</p><p>&#10060; No &#8220;I&#8217;ll merge it when the feature is done.&#8221;</p><p>The whole mechanism is small batches. Integrate a little, constantly, and changes never get big enough to become a conflict.</p><p>The merge that hurt when you did it once a week stops hurting when you do it three times a day &#8212; not because the work got easier, but because each piece is small enough that there&#8217;s nothing to collide with.</p><p>You&#8217;re not avoiding the painful thing. You&#8217;re doing it so often it stops being painful.</p><h2>&#8220;But how do we ship half-finished work?&#8221;</h2><p>This is the real objection. If I merge to main every day, and my feature takes two weeks, won&#8217;t I be pushing broken, half-built code into the shared trunk?</p><p>No &#8212; because you hide unfinished work <em>in</em> main, not <em>from</em> main.</p><p>With Trunk-based development you integrate incomplete work safely:</p><ul><li><p><strong>Feature flags</strong> &#8212; the code is merged and deployed, but dark. It doesn&#8217;t run until you enable it.</p></li><li><p><strong>Branch by abstraction</strong> &#8212; add an abstraction layer (like an interface) in front of old code, and swap the implementation behind it incrementally, with main green the entire time.</p></li><li><p><strong>Keystone interface</strong> &#8212; build the feature, but don&#8217;t expose it to users yet. &#8220;Turn on&#8221; the feature in the UI at the end.</p></li></ul><p>You integrate code that isn&#8217;t <em>finished</em> without integrating code that&#8217;s <em>broken</em>. The feature is incomplete; the trunk is always releasable. This makes sense once you stop thinking &#8220;merged&#8221; = &#8220;done&#8221;.</p><p>The team that says &#8220;we can&#8217;t do trunk-based, our features are too big to finish in a day&#8221; has it backwards.</p><p>The features don&#8217;t have to be finished in a day. The <em>integrations</em> do.</p><h2>How Long Do Your Branches Live?</h2><p>You don&#8217;t need to think too hard to know which camp you&#8217;re in. Open your repo&#8217;s branch list and find the oldest open branch. Look at its age.</p><p>If it&#8217;s measured in hours, you&#8217;re integrating. If it&#8217;s measured in days or sprints, you&#8217;re feature branching &#8212; and every one of those branches is a deferred merge that gets harder over time, no matter how green the build looks today.</p><p>Trunk-based development isn&#8217;t &#8220;no branches.&#8221; It&#8217;s no long-lived ones.</p><p>The thing that breaks CI was never branching itself &#8212; it&#8217;s how long the branch lived before it was merged into main. Shrink the branch lifetime, and the merge pain you&#8217;ve organised your whole workflow around avoiding simply stops existing.</p><div><hr></div><p>&#128073; Next week, 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 hacks. 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>]]></content:encoded></item><item><title><![CDATA[Hexagonal Architecture: Why I Don't Abstract the Database for Swappability]]></title><description><![CDATA[Misconception: &#8220;Repository interfaces exist so databases can be swapped&#8221;]]></description><link>https://journal.optivem.com/p/hexagonal-architecture-why-i-dont-abstract-for-swappability</link><guid isPermaLink="false">https://journal.optivem.com/p/hexagonal-architecture-why-i-dont-abstract-for-swappability</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Fri, 12 Jun 2026 14:20:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2ca19274-2e84-440c-b999-f0594c62b840_1000x666.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>You read the book. You applied Clean Architecture in real code. And it went downhill.</strong></p><p>That&#8217;s not on you &#8212; it&#8217;s the gap between the book and reality. ORM entities sneaking into the domain, business logic forced into memory until the system crawls, external DTOs leaking straight into your core. I see these three mistakes in almost every codebase that &#8220;did Clean Architecture.&#8221;</p><p>&#128197; Join me: <strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture: Stop doing it wrong</a></strong> on Wed 26th Aug, 5:00 - 6:30 PM (CEST)</p><p><strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">&#8594; Reserve your spot</a></strong></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><h2>Misconception: &#8220;Repository interfaces exist so databases can be swapped&#8221;</h2><p>One of the most common criticisms of Hexagonal Architecture and Clean Architecture goes like this:</p><blockquote><p>&#8220;Why are you abstracting over the database? You&#8217;re never going to swap it anyway.&#8221;</p></blockquote><p>And honestly?</p><p>If the goal is database swappability, I partly agree.</p><p>Most teams aren&#8217;t going to wake up tomorrow and replace PostgreSQL with MongoDB.</p><p>Most applications will use the same database for years.</p><p>So if database swappability is your main justification for repository interfaces, it&#8217;s not a particularly convincing one.</p><p>But that&#8217;s not why I use them.</p><h2>&#10060; Dragging the database into every change and every test</h2><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_!_sKV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_sKV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png 424w, https://substackcdn.com/image/fetch/$s_!_sKV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png 848w, https://substackcdn.com/image/fetch/$s_!_sKV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png 1272w, https://substackcdn.com/image/fetch/$s_!_sKV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_sKV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png" width="387" height="410.3937823834197" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/49d157f3-98fd-4610-822e-3fda260a6651_579x614.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:614,&quot;width&quot;:579,&quot;resizeWidth&quot;:387,&quot;bytes&quot;:29850,&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/197845964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.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_!_sKV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png 424w, https://substackcdn.com/image/fetch/$s_!_sKV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png 848w, https://substackcdn.com/image/fetch/$s_!_sKV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.png 1272w, https://substackcdn.com/image/fetch/$s_!_sKV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F49d157f3-98fd-4610-822e-3fda260a6651_579x614.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 aria-hidden="true" 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><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>You want to test:</p><pre><code><code>Premium customers receive 20% discount.
Regular customers receive 5% discount.</code></code></pre><p>That sounds simple.</p><p>But if the business logic directly calls ORM classes / uses Entity Framework or JPA, the test looks like this:</p><pre><code><code>1. Insert test customer
2. Execute business logic
3. Read result
4. Clean database</code></code></pre><p></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:&quot;36b91d92-1537-4167-8cd6-f2d6fb7fd4f6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">@SpringBootTest
@Testcontainers
class DiscountServiceDatabaseTest {

    @Container
    static PostgreSQLContainer&lt;?&gt; postgres =
        new PostgreSQLContainer&lt;&gt;("postgres:16");

    @DynamicPropertySource
    static void datasource(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired CustomerRepository customers;       // JPA repository
    @Autowired DiscountService discountService;    // business logic, talks to JPA directly

    @AfterEach
    void cleanUp() {
        customers.deleteAll();                     // 4. clean database
    }

    @Test
    void premiumCustomersReceive20PercentDiscount() {
        // 1. insert test customer
        var alice = customers.save(new CustomerEntity("Alice", Tier.PREMIUM));

        // 2. execute business logic (which loads the customer back out of the DB)
        var finalPrice = discountService.priceFor(alice.getId(), BigDecimal.valueOf(100));

        // 3. read result
        assertThat(finalPrice).isEqualTo(BigDecimal.valueOf(80));
    }

    @Test
    void regularCustomersReceive5PercentDiscount() {
        // 1. insert test customer
        var bob = customers.save(new CustomerEntity("Bob", Tier.REGULAR));

        // 2. execute business logic (which loads the customer back out of the DB)
        var finalPrice = discountService.priceFor(bob.getId(), BigDecimal.valueOf(100));

        // 3. read result
        assertThat(finalPrice).isEqualTo(BigDecimal.valueOf(95));
    }
}</code></pre></div><p></p><p>Just to verify a discount calculation.</p><p>The database isn&#8217;t the thing you&#8217;re interested in.</p><p>The business rule is.</p><p>But you&#8217;re forced to involve the database just to test it.</p><h2>&#9989; Repository interfaces exist so business logic can be tested independently from infrastructure</h2>
      <p>
          <a href="https://journal.optivem.com/p/hexagonal-architecture-why-i-dont-abstract-for-swappability">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Writing code is cheap. Maintaining it is where the cost accumulates.]]></title><description><![CDATA[Everything went fast in the first sprints. But suddenly, as time went on, it took longer and longer to make a change. How to solve this?]]></description><link>https://journal.optivem.com/p/maintaining-code-is-where-the-cost-accumulates</link><guid isPermaLink="false">https://journal.optivem.com/p/maintaining-code-is-where-the-cost-accumulates</guid><dc:creator><![CDATA[Valentina Jemuović]]></dc:creator><pubDate>Tue, 09 Jun 2026 14:23:51 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/95f8c098-214c-4211-9e08-53f943ad67d1_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>The cost of software is NOT in writing it.</p><p>The real cost is:<br>maintaining it,<br>changing it,<br>debugging it,<br>extending it,<br>and understanding it years later.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZbVP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZbVP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png 424w, https://substackcdn.com/image/fetch/$s_!ZbVP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png 848w, https://substackcdn.com/image/fetch/$s_!ZbVP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png 1272w, https://substackcdn.com/image/fetch/$s_!ZbVP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZbVP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png" width="1025" height="944" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:944,&quot;width&quot;:1025,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:202148,&quot;alt&quot;:null,&quot;title&quot;:null,&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/201172569?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.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_!ZbVP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png 424w, https://substackcdn.com/image/fetch/$s_!ZbVP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png 848w, https://substackcdn.com/image/fetch/$s_!ZbVP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.png 1272w, https://substackcdn.com/image/fetch/$s_!ZbVP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F246c5aaf-aba8-4264-8cd9-33094b0d8f19_1025x944.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 aria-hidden="true" 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><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>Does code quality matter?</p><p>I came to realize that software maintenance has a high cost because:</p><ul><li><p>Tightly coupled architecture &#8594; <strong>a &#8220;small&#8221; change ripples across half the codebase</strong></p></li><li><p>No tests or poor tests &#8594; no protection when making a change</p></li><li><p>Unreadable code &#8594; hard to make any change (update code or add new feature)</p></li></ul><p>With poor technical practices, software maintenance costs skyrocket, become unmanageable, and eventually, like an avalanche, destroy successful products.</p><p>Clean Architecture and testing are NOT optional.</p><p>That&#8217;s why I want to show you how to design your architecture such that it is maintainable &amp; testable.</p><p>Join the live session:</p><p><strong><a href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers">Clean Architecture for Backend Developers</a></strong></p><p>&#128467; Aug 26<br>&#9200; 5:00&#8211;6:30 PM (CEST)</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers&quot;,&quot;text&quot;:&quot;&#127942;Register now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://optivem.thinkific.com/products/live_events/clean-architecture-for-backend-developers"><span>&#127942;Register now</span></a></p><p></p><p></p>]]></content:encoded></item><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 aria-hidden="true" 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><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></channel></rss>