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

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

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

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

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

    private final Logger logger;

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

    public OrderResult execute(CreateOrderCommand cmd) {

        logger.info("CreateOrder started");

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

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

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

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

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

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

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

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

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

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

Expected totalPrice: 108.00
Actual totalPrice:   900.00</code></code></pre><p>The Tax API Stub returned <code>{ "rate": 8 }</code>, just like the real API would. <code>TaxGateway</code> read the value and returned <code>8</code> directly to <code>OrderService</code>, which calculated <code>100 * 8 = 800</code> in tax. In the unit test, the mock hid this by returning <code>0.08</code> directly. In the acceptance test, the real code path runs and the bug is caught.</p><h2>Why Unit Tests Can Never Catch This</h2><p>The bug isn&#8217;t in <code>OrderService</code>. The math is correct. The wiring is correct. The bug is inside <code>TaxGateway</code> &#8212; in how it parses the external API response.</p><p>But the unit test for <code>OrderService</code> mocks out <code>TaxGateway</code> entirely. The HTTP call never fires. The JSON parsing never runs. The bug is invisible.</p><p><strong>The unit test:</strong></p><ul><li><p>Tests <code>OrderService</code> in isolation</p></li><li><p>Replaces <code>TaxGateway</code> with a mock</p></li><li><p>The mock returns what the developer <em>assumes</em> the gateway returns</p></li><li><p>The real HTTP call and JSON parsing never execute</p></li><li><p>Can&#8217;t catch a bug inside the mocked component</p></li></ul><p><strong>The acceptance test:</strong></p><ul><li><p>Tests the full feature against the real system with stubs for external dependencies</p></li><li><p>The real <code>TaxGateway</code> code runs &#8212; HTTP call, JSON parsing, everything</p></li><li><p>The Tax API Stub behaves like the real Tax API &#8212; returning <code>{ "rate": 8 }</code></p></li><li><p>Catches bugs anywhere in the chain, not just in the class under test</p></li></ul><p>Unit tests verify your logic given your assumptions. Acceptance tests verify the feature works regardless of your assumptions. No amount of unit testing can catch a bug that&#8217;s hidden behind a mock.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8jew!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8jew!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 424w, https://substackcdn.com/image/fetch/$s_!8jew!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 848w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1272w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8jew!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png" width="964" height="1142" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1142,&quot;width&quot;:964,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:127085,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://journal.optivem.com/i/191975581?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8jew!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 424w, https://substackcdn.com/image/fetch/$s_!8jew!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 848w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1272w, https://substackcdn.com/image/fetch/$s_!8jew!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd618e-1b86-4668-96bf-44e0fda2ffb0_964x1142.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>But Wait &#8212; How Do We Know the Stub Is Correct?</h2><p>Fair question. If the acceptance test uses a Tax API Stub instead of the real Tax API, couldn&#8217;t the stub itself be wrong?</p><p>That&#8217;s where <strong>contract tests</strong> come in. A contract test runs the same scenarios against both the real Tax API and the Tax API Stub, and verifies they return the same results. If the stub drifts from reality, the contract test fails.</p><p>So the full safety net looks like this:</p><ul><li><p><strong>Unit tests</strong> verify each component&#8217;s logic in isolation</p></li><li><p><strong>Acceptance tests</strong> verify the feature works end-to-end, using stubs for external systems</p></li><li><p><strong>Contract tests</strong> verify the stubs behave like the real external systems</p></li></ul><p>Each layer catches a different category of bug. Skip any one of them, and you have a blind spot.</p><h2>This Isn&#8217;t an Edge Case</h2><p>The bug in our example was a wrong assumption about the API response format. But this is just one of many ways external system integration breaks:</p><ul><li><p><strong>Wrong format assumption</strong> &#8212; The Tax API returns <code>{ "rate": 8 }</code> meaning 8%, but your code treats it as <code>0.08</code>. Your mock returns whatever you assumed, so the unit test passes.</p></li><li><p><strong>Hardcoded value</strong> &#8212; The developer hardcoded <code>return 0.10</code> inside <code>TaxGateway</code> while building the feature, planning to wire up the HTTP call &#8220;later.&#8221; Later never came. The unit test mocks out <code>TaxGateway</code> entirely, so the hardcoded value is never executed.</p></li><li><p><strong>Wrong field</strong> &#8212; The API returns <code>{ "taxRate": 8, "importRate": 12 }</code> and your code reads <code>importRate</code> instead of <code>taxRate</code>. Your mock only returns the field you expected, so the unit test passes.</p></li><li><p><strong>Stale mapping</strong> &#8212; The API used to return <code>{ "rate": 8 }</code> but v2 changed it to <code>{ "tax": { "rate": 8 } }</code>. Your code still reads the top-level field. Your mock still returns the old format, so the unit test passes.</p></li></ul><p>This pattern shows up constantly across all types of external integrations:</p><ul><li><p>A <strong>payment gateway</strong> that returns amounts in cents, but your code treats them as dollars.</p></li><li><p>A <strong>shipping API</strong> that changed its response format in v2, but your mocks still return v1.</p></li><li><p>An <strong>inventory service</strong> that returns <code>"OUT_OF_STOCK"</code> as a string, but your mock returns a boolean <code>false</code>.</p></li><li><p>A <strong>currency API</strong> that returns rates with 6 decimal places, but your mock rounds to 2.</p></li></ul><p>Every one of these passes unit tests &#8212; because the mock <em>is</em> the assumption. If the assumption is wrong, the mock is wrong, and the test is worthless.</p><p>Every one of these ships to production. Every one of these gets caught by acceptance tests.</p><h2>The Takeaway</h2><p>Unit tests answer: <em>&#8220;Does this component do its job correctly, given my assumptions?&#8221;</em></p><p>Acceptance tests answer: <em>&#8220;Does this feature work the way the customer expects?&#8221;</em></p><p>Contract tests answer: <em>&#8220;Are my assumptions about external systems correct?&#8221;</em></p><p>These are fundamentally different questions. If you&#8217;re only asking the first one, you&#8217;re leaving the other two for your customers to answer.</p><h2>Want to learn how to write acceptance tests like this?</h2><p>I&#8217;m running a live workshop: <strong><a href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop">Stop Shipping Bugs: Acceptance Tests Workshop</a>.</strong></p><p>4 hours. Two evenings. Live on Zoom, with me.</p><p>&#9889; I&#8217;ll walk you through the full architecture &#8212; DSL, Drivers, how it all fits together &#8212; and by the end, you&#8217;ll have written a real acceptance test from scenario to assertion.</p><p>&#128197; May 25-26, 2:00-4:00 PM CET</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop&quot;,&quot;text&quot;:&quot;Join the workshop &#8594;&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://optivem.thinkific.com/products/courses/2026-05-27-acceptance-testing-workshop"><span>Join the workshop &#8594;</span></a></p><p>Limited spots. Register now with the early bird discount - <strong>100 EUR off with code EARLYBIRD100</strong></p><p>&#8212; Valentina</p>]]></content:encoded></item></channel></rss>