<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sara A.</title>
    <description>The latest articles on Forem by Sara A. (@srsandrade).</description>
    <link>https://forem.com/srsandrade</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3726262%2F9ed467e4-a85c-4886-891f-9d0a839610bf.png</url>
      <title>Forem: Sara A.</title>
      <link>https://forem.com/srsandrade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/srsandrade"/>
    <language>en</language>
    <item>
      <title>The Test That Lied to Me: A practical guide to writing unit tests that actually mean something</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Sat, 07 Mar 2026 19:22:15 +0000</pubDate>
      <link>https://forem.com/srsandrade/the-test-that-lied-to-me-a-practical-guide-to-writing-unit-tests-that-actually-mean-something-5f26</link>
      <guid>https://forem.com/srsandrade/the-test-that-lied-to-me-a-practical-guide-to-writing-unit-tests-that-actually-mean-something-5f26</guid>
      <description>&lt;h1&gt;
  
  
  The Test That Lied to Me
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A practical guide to writing unit tests that actually mean something&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A quick note before we start: there is a lot of enthusiasm lately about AI generating unit tests automatically. I have tried it. The results were technically valid, consistently green, and almost completely useless — which, if you think about it, is a perfect description of most of the unit tests in the industry.&lt;/p&gt;

&lt;p&gt;So here we are. Maybe this helps the AI write better tests. Maybe it just helps the engineers. Either way, I felt the need to write it down.&lt;/p&gt;

&lt;p&gt;This guide is not about coverage percentages or which framework to use. It is about the decisions that determine whether your test suite is an asset or an alibi.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The examples in this guide use Java with JUnit 5 and Mockito. The principles apply everywhere.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 1: The Name Is the Specification
&lt;/h2&gt;

&lt;p&gt;Before a test does anything, it declares what it expects. That declaration lives in the name.&lt;/p&gt;

&lt;p&gt;A bad test name is a lost opportunity. Not just for documentation — for thinking. If you cannot write a clear name for a test, it is usually because you have not yet decided what you are actually testing.&lt;/p&gt;

&lt;p&gt;The convention that works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Should_ExpectedBehaviour_When_StateUnderTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not because the format is sacred, but because it forces two decisions: what should happen, and under what condition. Both need to be explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Tells you nothing&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;testValidation&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Tells you everything&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ReturnValidationError_When_AmountIsNegative&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second name is a spec. If this test fails, you know exactly what broke before reading a single line of code. That matters at 11pm when something is on fire.&lt;/p&gt;

&lt;p&gt;One more thing: consistency. A codebase where half the tests say &lt;code&gt;should_&lt;/code&gt; and half say &lt;code&gt;test_&lt;/code&gt; and half say &lt;code&gt;verify_&lt;/code&gt; is a codebase where nobody agreed on anything. Pick a convention and apply it. It costs nothing and saves more than you expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: One Test, One Reason to Fail
&lt;/h2&gt;

&lt;p&gt;The most common mistake in test suites is not bad assertions. It is too many of them, testing too many things at once.&lt;/p&gt;

&lt;p&gt;A test that can fail for three different reasons gives you almost no information when it does fail. You know something is wrong. You do not know what.&lt;/p&gt;

&lt;h3&gt;
  
  
  One condition, one test
&lt;/h3&gt;

&lt;p&gt;Even if two different invalid inputs produce the same error, they belong in separate tests. The failure condition is not the same, even if the output is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Two causes, one test&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ThrowException_When_Invalid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unknownId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deletedId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ One cause, one test&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ThrowNotFoundException_When_OrderDoesNotExist&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unknownId&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unknownId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ThrowNotFoundException_When_OrderIsDeleted&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deletedId&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deletedOrder&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;

    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deletedId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests are longer this way. That is the point. Diagnostic value is worth the extra lines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameterization: the right use
&lt;/h3&gt;

&lt;p&gt;Parameterized tests are useful when you are testing the same behaviour with different values of the same input. They are not useful when you are testing different behaviours and bundling them together to make the test file look shorter.&lt;/p&gt;

&lt;p&gt;The rule: one axis of variation per parameterized test. If your test method takes a flag name as a parameter alongside the flag value, you are probably mixing two different concerns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Mixed concerns — iterates over unrelated flags&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Arguments&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"include_tax"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"include_tax"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"include_discount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"include_discount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_HandleFlag_When_Set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// tests multiple unrelated flags in one method&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ One concern, two values&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Arguments&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;taxFlagVariants&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"with-tax.json"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"without-tax.json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_GenerateCorrectInvoice_When_TaxFlagSet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;includeTax&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;expectedFile&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// tests one flag, both states&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the parameterized test fails, you want to know exactly which value caused it. A test that iterates over unrelated flags gives you a row number. A focused test gives you a reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Structure Is Not Optional
&lt;/h2&gt;

&lt;p&gt;A test has three jobs: set up the world, do the thing, check what happened. If you cannot identify which line belongs to which job, the test is already a problem.&lt;/p&gt;

&lt;p&gt;Given, When, Then is not ceremony. It is the minimum structure required for a test to be readable by someone who did not write it — including future you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_CalculateAggregatedData_When_ValidDataProvided&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Given&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;returnDataToBeAggregated&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DataPoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sensor1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DataPoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sensor2"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="nc"&gt;AggregatorService&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregatorService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// When&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateAggregatedData&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Then&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The comments (&lt;code&gt;// Given&lt;/code&gt;, &lt;code&gt;// When&lt;/code&gt;, &lt;code&gt;// Then&lt;/code&gt;) are for illustration only. In real tests, the structure should be clear enough that the comments are unnecessary. If they are not, the test probably needs restructuring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Naming conventions within the test
&lt;/h3&gt;

&lt;p&gt;Two naming decisions that pay consistent dividends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick a consistent name for the class under test and use it everywhere. Common choices are &lt;code&gt;victim&lt;/code&gt;, &lt;code&gt;underTest&lt;/code&gt;, or &lt;code&gt;sut&lt;/code&gt; (system under test). Which one you pick matters less than the fact that everyone on the team picks the same one — it makes the subject of the test immediately identifiable at a glance.&lt;/li&gt;
&lt;li&gt;Do the same for the output. &lt;code&gt;result&lt;/code&gt; is a common choice. Whatever you pick, the consistency is what makes tests scannable.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AggregatorService&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregatorService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateAggregatedData&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keep variables where they matter
&lt;/h3&gt;

&lt;p&gt;There is a tendency to extract every string into a named variable. In production code, this is usually right. In tests, it can be counterproductive.&lt;/p&gt;

&lt;p&gt;If a value is self-explanatory, inline it. Extracting &lt;code&gt;"ADMIN"&lt;/code&gt; into a variable called &lt;code&gt;adminRoleName&lt;/code&gt; adds a line and removes information. The reader now has to look up what &lt;code&gt;adminRoleName&lt;/code&gt; is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Over-extracted&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;adminRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ADMIN"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"USER"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"usr-001"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"usr-002"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"usr-003"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;insertUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adminRole&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adminRole&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userRole&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Inlined — the values speak for themselves&lt;/span&gt;
&lt;span class="n"&gt;insertUsers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"usr-001"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ADMIN"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"usr-002"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ADMIN"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"usr-003"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"USER"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requestUsersByRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ADMIN"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extracting&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsOnly&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"usr-001"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"usr-002"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4: The Setup Trap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@BeforeEach&lt;/code&gt; is one of the most misused tools in unit testing. It exists to avoid repeating identical setup across tests. It is routinely used to hide setup that is not actually identical — just similar enough to seem like it should be shared.&lt;/p&gt;

&lt;p&gt;The result is tests that look short but are not self-contained. To understand what a test actually does, you have to read the test and the setup method and remember how they interact. If a later test overrides one of the behaviours defined in setup, that is three places to look.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Hidden dependency — where does the mock behaviour come from?&lt;/span&gt;
&lt;span class="nd"&gt;@BeforeEach&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anyLong&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="n"&gt;victim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ThrowException_When_NotFound&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Silently overrides BeforeEach&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anyLong&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99L&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Self-contained — everything you need is right here&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ReturnEntity_When_IdIsValid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="nc"&gt;MyService&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;Entity&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;assertNotNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ThrowException_When_NotFound&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99L&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="nc"&gt;MyService&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99L&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version is longer. It is also the one that tells you, immediately, what each test needs to be true. There is no invisible context. There is no setup to remember.&lt;/p&gt;

&lt;p&gt;The alternative to &lt;code&gt;@BeforeEach&lt;/code&gt; is not duplication — it is helper functions used correctly. A helper that builds an object with sensible defaults and accepts explicit parameters for the values that matter keeps the test readable without hiding what is being tested.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Helper hides what matters&lt;/span&gt;
&lt;span class="n"&gt;prepareData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;createInvalidData&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;createPartialData&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Helper is explicit about what varies&lt;/span&gt;
&lt;span class="n"&gt;prepareData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;createOrderLine&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item-a"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;createOrderLine&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item-b"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is what the reader learns from looking at the test. The first version tells you the data is invalid and partial. The second tells you exactly why — one line has a zero price. That is the information that matters for understanding what the test is actually verifying.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;@BeforeEach&lt;/code&gt; has legitimate uses: initialising mocks, setting up state that is genuinely shared across every test, preparing infrastructure. The problem is using it to define mock behaviour — which is almost never truly shared.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;@BeforeAll&lt;/code&gt; has even fewer legitimate uses at the unit test level. Loading a config file once, compiling a regex, spinning up something expensive. Not setting up test data. If your unit test needs data set up once for all tests, that is usually a sign the tests are not as independent as they should be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test utilities — use sparingly, test thoroughly
&lt;/h3&gt;

&lt;p&gt;There is a tension worth acknowledging here. On one hand, test utility classes tend to become bloated, overgeneralised, and quietly wrong. On the other hand, utility classes extracted from production code need to be tested — properly, not incidentally.&lt;/p&gt;

&lt;p&gt;These are different problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On production utility classes:&lt;/strong&gt; a utility method extracted for reuse is a first-class unit of logic. It deserves its own tests covering valid inputs, invalid inputs, and boundary conditions. The fact that it is called from another class does not mean it is implicitly tested — it means it is tested indirectly, which is not the same thing. Indirect coverage tells you something broke. Direct tests tell you what.&lt;/p&gt;

&lt;p&gt;That said, direct unit tests alone are not enough. A utility method that works correctly in isolation can still behave unexpectedly in context — with real data, in combination with other logic, under conditions the unit tests did not anticipate. Test it directly and verify it in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On test utility classes:&lt;/strong&gt; the bar should be high. A helper that creates a test object with sensible defaults is useful. A helper that encodes business logic, makes assertions, or accumulates enough behaviour to need its own documentation is a liability. When a test utility class becomes complex enough that you have to understand it to understand a test, it has defeated its own purpose.&lt;/p&gt;

&lt;p&gt;The rule of thumb: if the logic is genuinely shared across many tests and has no natural home elsewhere, a utility makes sense. If it exists mainly to save a few lines, inline it. Explicit setup in the test is almost always easier to read than a call to a helper that hides what is actually being prepared.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Keep Tests Simple
&lt;/h2&gt;

&lt;p&gt;A test that contains an &lt;code&gt;if&lt;/code&gt; statement can be wrong in two different ways: the production code can be wrong, or the test logic can be wrong. At that point, who is testing the test?&lt;/p&gt;

&lt;p&gt;Tests should verify behaviour, not implement it. The moment a test starts making decisions — branching on input, looping over results, switching on type — it has become code that needs its own tests. That is not a metaphor. That is the actual problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The test is doing too much thinking&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ApplyDiscount_When_Eligible&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;testCustomers&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEligible&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateDiscount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateDiscount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Two cases, two tests, no ambiguity&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ApplyDiscount_When_CustomerIsEligible&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"alice"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eligible&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateDiscount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_NotApplyDiscount_When_CustomerIsNotEligible&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bob"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eligible&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateDiscount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When test logic grows complex, the instinct is to make it smarter. The right move is almost always to make it simpler — and split it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: Mock the Behaviour, Not the Data
&lt;/h2&gt;

&lt;p&gt;Mocking is one of the easier things to do wrong in unit testing. The tell is when you find yourself mocking a data object — a DTO, a plain record, a value container.&lt;/p&gt;

&lt;p&gt;Mocking data objects produces fragile tests. They break when the object's structure changes, even when the logic being tested has not changed at all. Worse, they test almost nothing meaningful. A mock DTO that returns a hardcoded string is not telling you anything about your system.&lt;/p&gt;

&lt;p&gt;What you actually want to mock is the behaviour of the things your code depends on — repositories, external services, anything that makes a decision or crosses a boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Mocking data — fragile and meaningless&lt;/span&gt;
&lt;span class="nc"&gt;OrderDTO&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mockito&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderDTO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProductId&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getQuantity&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// If OrderDTO gains a new field, this test might break for no reason&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Mocking behaviour — tests how the service handles real scenarios&lt;/span&gt;
&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Product A"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction matters most when thinking about what you are actually verifying. A test that mocks a DTO is verifying that Mockito works. A test that mocks a repository is verifying that your service behaves correctly when data is and is not found.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying interactions
&lt;/h3&gt;

&lt;p&gt;Not everything worth testing produces a return value. Sometimes the important thing is that a method was called, was called with the right arguments, or was never called at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_NotPersist_When_InputIsInvalid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;processData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;never&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anyString&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_PersistParsedValue_When_InputIsValid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ArgumentCaptor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;captor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ArgumentCaptor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forClass&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;processData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"raw-value"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;captor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;capture&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"parsed-value"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;captor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first test tells you nothing was saved. The second tells you exactly what was saved. Neither requires a return value. Both tell you something real about how the system behaves.&lt;/p&gt;

&lt;p&gt;That said, if verifying an interaction is the only way to test something meaningful — a calculation, a transformation, a decision — it is worth pausing before reaching for &lt;code&gt;verify&lt;/code&gt;. Code that can only be validated through its side effects is often code that is doing too much in one place. Extracting the logic into a dedicated class or utility makes it directly testable by its output, which is almost always cleaner.&lt;/p&gt;

&lt;p&gt;Needing to spy on a method to confirm a calculation happened is not just a testing problem. It is usually a separation of concerns problem wearing a testing costume.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7: The Edge Cases Are the Point
&lt;/h2&gt;

&lt;p&gt;Unit tests are the cheapest place to cover edge cases. Cheaper than component tests, cheaper than integration tests, infinitely cheaper than a production incident.&lt;/p&gt;

&lt;p&gt;Yet most test suites are weighted toward the happy path. The valid input goes in, the correct output comes out. Green. Done. The test suite grows, coverage climbs, and the system still breaks on the first null it encounters.&lt;/p&gt;

&lt;p&gt;Edge cases worth explicitly testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Null inputs, empty strings, empty collections&lt;/li&gt;
&lt;li&gt;Boundary values: zero, negative numbers, maximum allowed values&lt;/li&gt;
&lt;li&gt;The case where a dependency returns nothing (&lt;code&gt;Optional.empty()&lt;/code&gt;, empty list)&lt;/li&gt;
&lt;li&gt;The case where a dependency throws&lt;/li&gt;
&lt;li&gt;Inputs that are technically valid but semantically unusual
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ThrowException_When_OrderLineIsNull&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateUnitPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ThrowException_When_PriceIsNull&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;OrderLine&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderLine&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateUnitPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ReturnZeroUnitPrice_When_QuantityIsZero&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;OrderLine&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderLine&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totalPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateUnitPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ZERO&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edge cases are not extra credit. They are the specification for how your code behaves when the world does not cooperate. Which is most of the time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8: Tests That Lie
&lt;/h2&gt;

&lt;p&gt;A passing test should mean something. If it passes when the feature is broken, it is not a test — it is a green light that gives false confidence, which is worse than no test at all.&lt;/p&gt;

&lt;p&gt;Tests lie in predictable ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  The assertion that does not assert
&lt;/h3&gt;

&lt;p&gt;The most common form. The test runs, it calls the method, but the assertion is checking something that is always true regardless of what the method does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ This always passes — it has nothing to do with process()&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_ProcessOrder_When_Valid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Product"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// We're asserting the id we assigned, not anything the method did&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ This actually tests something&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Should_PersistOrder_When_Valid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Product"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The test that survives deletion
&lt;/h3&gt;

&lt;p&gt;Delete the implementation. Run the tests. If the same tests still pass, they were not testing the implementation.&lt;/p&gt;

&lt;p&gt;This sounds extreme, but it is one of the most useful checks you can do on a test suite. Tests that survive the deletion of the thing they claim to test are guaranteed liars.&lt;/p&gt;

&lt;h3&gt;
  
  
  The complex test
&lt;/h3&gt;

&lt;p&gt;Tests that contain &lt;code&gt;if&lt;/code&gt; statements, &lt;code&gt;switch&lt;/code&gt; cases, or loops are tests that can have bugs. A test with a bug is not a test — it is a liability that produces a false sense of coverage.&lt;/p&gt;

&lt;p&gt;If the test logic is becoming complex, the answer is almost always to split it into simpler tests, not to make the existing test smarter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 9: Write Code That Wants to Be Tested
&lt;/h2&gt;

&lt;p&gt;Some code is hard to test because the test is poorly written. But some code is hard to test because the code itself is poorly designed — and the difficulty of testing is the most honest feedback you will get about that.&lt;/p&gt;

&lt;p&gt;A class that requires a running database to do anything is a class that has not separated its concerns. A method that produces no output and mutates hidden state is a method that has made itself invisible to assertions. A function that does five things is a function that needs five different test setups to cover each path.&lt;/p&gt;

&lt;p&gt;Testability is not a property you add after the fact. It emerges from design decisions made while writing the code.&lt;/p&gt;

&lt;p&gt;A few patterns that consistently produce untestable code — and what to do instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden dependencies.&lt;/strong&gt; If a class creates its own collaborators internally, there is no way to replace them in tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ No way to control what the repository does&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Inject it — now tests can provide their own&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logic buried in private methods.&lt;/strong&gt; Private methods are not directly testable. If a private method contains meaningful logic, that logic either gets tested indirectly through the public interface — which is fine — or it is complex enough that it should be extracted into its own class and tested directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Complex logic hidden where tests cannot reach it&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="nf"&gt;applyTieredPricing&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 30 lines of pricing logic&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Extracted — now testable on its own terms&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TieredPricingCalculator&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// same logic, now directly testable&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Static method calls and global state.&lt;/strong&gt; Static calls are impossible to mock and global state makes tests order-dependent (not impossible, but highly inadvisable). Both are usually avoidable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Methods that do too much.&lt;/strong&gt; A method that validates input, fetches data, applies business rules, formats output, and persists the result cannot be tested cleanly. Each responsibility it sheds becomes a unit that can be tested independently.&lt;/p&gt;

&lt;p&gt;The uncomfortable version of this principle: if writing the test feels like a struggle, read the production code before blaming the test. The test is probably trying to tell you something.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 10: The Test Suite As a Document
&lt;/h2&gt;

&lt;p&gt;The best test suites are the best onboarding material. Not because someone planned them that way, but because tests that are named correctly, structured clearly, and focused on one thing each naturally become a readable description of what the system does.&lt;/p&gt;

&lt;p&gt;When a new developer joins the team and wants to understand how order processing works, they have two options. They can read the production code, which tells them how. Or they can read the tests, which tell them what — what inputs are valid, what happens on the edge cases, what the system refuses to do and why.&lt;/p&gt;

&lt;p&gt;That is the difference between a test suite written for coverage and a test suite written with intention.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A useful exercise: read through your test names without reading the test bodies. Can you reconstruct what the system does from the names alone? If not, the names are not doing their job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Inheritance in tests works against this. When a test class extends a base class to inherit setup, the test is no longer self-contained. Understanding it requires reading two files, understanding how they relate, and tracking what the parent does and does not override. That is more archaeology than onboarding.&lt;/p&gt;

&lt;p&gt;As discussed in Part 4, the same applies to test utility classes that grow large enough to have their own logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 11: What Not to Test
&lt;/h2&gt;

&lt;p&gt;As important as knowing what to test is knowing what not to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not test the web layer
&lt;/h3&gt;

&lt;p&gt;A service does not return a 404. It throws a &lt;code&gt;NotFoundException&lt;/code&gt;. What happens to that exception — how it gets mapped to an HTTP status code, how the response body is shaped, what headers are attached — is the web layer's responsibility, not the service's.&lt;/p&gt;

&lt;p&gt;Unit tests live below that boundary. At the unit level, test the logic: services, domain classes, utility methods, calculations, decisions. Anything that can be verified in isolation, without starting a server or talking to a database, belongs here. The moment a test depends on another party — an HTTP layer, a real database, a message broker, an external service — it has crossed into component or integration territory. Keep the two separate and both become easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not bootstrap the application context
&lt;/h3&gt;

&lt;p&gt;If your unit test has &lt;code&gt;@SpringBootTest&lt;/code&gt; on it, it is not a unit test anymore. It is a component test that happens to live in the wrong folder.&lt;/p&gt;

&lt;p&gt;Bootstrapping a full application context to test a single service method is overkill by definition. It is slow, it introduces dependencies that have nothing to do with what you are testing, and it blurs the line between levels of testing in ways that tend to get worse over time.&lt;/p&gt;

&lt;p&gt;Unit tests should start fast and run in isolation. Mocks and stubs exist precisely so you do not need a running application to verify that a service behaves correctly. If you find yourself reaching for &lt;code&gt;@SpringBootTest&lt;/code&gt; at the unit level, the question worth asking is not "how do I make this work" but "why does this feel necessary" — because the answer usually points to a design problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This applies beyond Spring. Any equivalent mechanism that boots a full application context — dependency injection containers, embedded servers, framework runners — has no place in a unit test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Do not test the framework
&lt;/h3&gt;

&lt;p&gt;If you are using a framework such as Spring Boot, do not test that &lt;code&gt;@Autowired&lt;/code&gt; works or that the application can read the &lt;code&gt;application.yml&lt;/code&gt;. Spring tests that. If you are using Jackson, do not test that it serializes an object. Jackson tests that. The job of your tests is to verify that your code, given correct inputs from the framework, produces the right outputs.&lt;/p&gt;

&lt;p&gt;Testing framework behaviour wastes time, adds maintenance burden, and produces tests that break when you upgrade a dependency — not because your code changed, but because the framework's internals did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not write production code for tests
&lt;/h3&gt;

&lt;p&gt;If the only reason a getter exists is to make a test easier to write, that getter should not exist. The test should find another way.&lt;/p&gt;

&lt;p&gt;Production code written for tests is production code that does not serve production. It inflates the API, exposes internals that should stay internal, and misleads anyone who reads the class and wonders what that method is for.&lt;/p&gt;

&lt;p&gt;The constraint is useful. If a class is difficult to test without special access, that is usually feedback about the design. A class that is hard to test without poking at its internals is often a class that is doing too much, or a class whose dependencies are not properly injected.&lt;/p&gt;

&lt;p&gt;Test the observable behaviour. If that is not enough to verify the class is working, the class probably needs to be redesigned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Uncomfortable Part
&lt;/h2&gt;

&lt;p&gt;Most of these guidelines are not hard to understand. They are hard to apply consistently when you are under pressure, when the ticket is already late, when the test suite has a hundred tests written the wrong way and adding one more wrong one is faster than fixing the pattern.&lt;/p&gt;

&lt;p&gt;The longer version of this guide would be about that. About how a test suite degrades gradually, one convenience at a time, until the green board means almost nothing and everyone has quietly stopped trusting it.&lt;/p&gt;

&lt;p&gt;The short version is this: a test that you do not trust is not a safety net. It is a ritual.&lt;/p&gt;

&lt;p&gt;Writing tests that you actually believe is harder than writing tests that pass. It requires deciding what you are testing before you write the test. It requires resisting the urge to share setup that is not really shared. It requires accepting that ten focused tests are better than one test that checks everything.&lt;/p&gt;

&lt;p&gt;None of that is complicated.&lt;/p&gt;

&lt;p&gt;It just requires not taking shortcuts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Guideline&lt;/th&gt;
&lt;th&gt;The point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name clearly&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Should_ExpectedBehaviour_When_StateUnderTest&lt;/code&gt; — readable without reading the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One reason to fail&lt;/td&gt;
&lt;td&gt;Separate tests for separate failure causes, even when the outcome is the same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Given / When / Then&lt;/td&gt;
&lt;td&gt;Three sections, always. If you cannot identify them, restructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistent naming&lt;/td&gt;
&lt;td&gt;Pick a name for the class under test and the output, and use them everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-contained setup&lt;/td&gt;
&lt;td&gt;Avoid &lt;code&gt;@BeforeEach&lt;/code&gt; for mock behaviour. Put setup where it belongs: in the test.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mock behaviour, not data&lt;/td&gt;
&lt;td&gt;Mock repositories, services, decisions — not DTOs or plain objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline simple values&lt;/td&gt;
&lt;td&gt;Do not extract constants that are already readable as literals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No logic in tests&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;if&lt;/code&gt;, no loops, no &lt;code&gt;switch&lt;/code&gt;. If the test needs to think, split it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cover edge cases&lt;/td&gt;
&lt;td&gt;Null, empty, zero, boundary, missing. These are the interesting cases.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do not test the web layer&lt;/td&gt;
&lt;td&gt;Services throw exceptions. HTTP status codes are someone else's job.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do not bootstrap the context&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@SpringBootTest&lt;/code&gt; in a unit test is a component test in the wrong folder.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do not test the framework&lt;/td&gt;
&lt;td&gt;Test your logic. Assume Spring, Jackson, and JPA work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No test-only production code&lt;/td&gt;
&lt;td&gt;If a getter exists only for tests, the test is wrong, not the class.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>java</category>
      <category>testing</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>You Can’t Learn Spring Boot in a Weekend (And That’s Not the Problem)</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Fri, 06 Mar 2026 22:56:46 +0000</pubDate>
      <link>https://forem.com/srsandrade/you-cant-learn-spring-boot-in-a-weekend-and-thats-not-the-problem-4726</link>
      <guid>https://forem.com/srsandrade/you-cant-learn-spring-boot-in-a-weekend-and-thats-not-the-problem-4726</guid>
      <description>&lt;h2&gt;
  
  
  The “Give Me Two Days” Learning Strategy
&lt;/h2&gt;

&lt;p&gt;Today an engineer told me about his plans for the weekend. They were going to take a crash course in Java and Spring Boot.&lt;/p&gt;

&lt;p&gt;The kind that promises you will “cover the essentials” of an entire framework ecosystem in a few hours, usually with a very confident youtube instructor and a progress bar that moves at an inspiring pace.&lt;/p&gt;

&lt;p&gt;I told them that might not be the best plan. Not because learning is bad. Learning is always good.&lt;br&gt;&lt;br&gt;
But because the plan itself revealed a small misunderstanding about where the actual difficulty lies.&lt;/p&gt;

&lt;p&gt;To be fair, the frustration that triggered this idea was real.&lt;br&gt;&lt;br&gt;
They had been working on a piece of an application and later realised there were some issues they could have spotted earlier. You know the situation.&lt;/p&gt;

&lt;p&gt;You look at a block of code and something feels… off. But you can't immediately explain why. &lt;/p&gt;

&lt;p&gt;Later, after someone points it out - or the behaviour becomes obvious - you look back and think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How did I not notice this earlier?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Their conclusion was simple: he needed to learn the framework better. Which is a very common reaction in software development.&lt;br&gt;
Something goes wrong → we assume the tool is the problem → we try to learn the tool faster.&lt;/p&gt;

&lt;p&gt;And to be fair, this wasn’t the first time I had heard a version of this plan. Some time ago another engineer, still early in their journey through larger systems, came with a carefully designed &lt;strong&gt;two-week training route&lt;/strong&gt;. &lt;br&gt;
It had a schedule, a list of libraries, and a surprisingly ambitious timeline. By the end of those two weeks, according to the document, they would have covered half the modern Java ecosystem.&lt;/p&gt;

&lt;p&gt;The problem was that most of that route focused on &lt;em&gt;tools and libraries&lt;/em&gt;, not on the foundations underneath them.&lt;br&gt;
Which is understandable. Libraries feel concrete. Frameworks feel productive. They promise visible progress.&lt;/p&gt;

&lt;p&gt;But the thing is: the code that didn’t make sense would not suddenly make sense if it had been written in Spring Boot, Quarkus, Micronaut, plain Java, C#, or C# with .NET.&lt;/p&gt;

&lt;p&gt;Confusing code has a remarkable ability to remain confusing across frameworks.&lt;br&gt;
And the skill required to recognise it earlier usually has very little to do with crash courses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frameworks Are Just Fancy Packaging
&lt;/h2&gt;

&lt;p&gt;As referred, the common assumption in these situations is that the missing piece must be the framework.&lt;br&gt;
If the system uses Spring Boot, then understanding Spring Boot better should make the system easier to understand.&lt;/p&gt;

&lt;p&gt;But frameworks like Spring Boot are not the system. They are an &lt;strong&gt;abstraction layer over problems engineers have been solving for decades&lt;/strong&gt;:&lt;br&gt;
Dependency injection. Configuration management. Application lifecycle. HTTP routing. Persistence. Messaging.&lt;/p&gt;

&lt;p&gt;Which is great. But it also creates a small illusion: it makes it look like the complexity lives in the framework.&lt;/p&gt;

&lt;p&gt;In reality, the complexity usually lives &lt;strong&gt;outside it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is another trap hidden here as well: sometimes concepts appear to change meaning depending on the ecosystem you are working in. Take something like a “queue.”&lt;/p&gt;

&lt;p&gt;In embedded systems, a queue might be a small in-memory structure used to pass messages between tasks or threads.&lt;br&gt;
In web applications, someone says “queue” and suddenly they mean RabbitMQ, Kafka, or some distributed messaging system moving events between services.&lt;br&gt;
Different scale. Different guarantees. Same idea: a buffer that decouples producers from consumers.&lt;/p&gt;

&lt;p&gt;Frameworks and platforms wrap these ideas with different tools, terminology, and levels of complexity. It can feel like you are learning a completely new concept, when in reality you are seeing the same one operating at a different scale.&lt;/p&gt;

&lt;p&gt;When you are reading code that doesn’t make sense, the issue is rarely that you forgot how a Spring annotation works. It is much more likely that you are dealing with unclear responsibilities, tangled logic, hidden assumptions, or design decisions that were never properly explained.&lt;/p&gt;

&lt;p&gt;A crash course can show you that &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, and &lt;code&gt;@Controller&lt;/code&gt; exist. It can show you roughly where they go.&lt;br&gt;
What it rarely explains is what those annotations actually trigger, why the separation exists, or when the design itself is the real problem.&lt;/p&gt;

&lt;p&gt;It also cannot teach you why a piece of business logic ended up in the wrong layer, why two modules depend on each other in surprising ways, or why a seemingly simple change suddenly affects five different parts of the system.&lt;/p&gt;

&lt;p&gt;Frameworks do not eliminate design problems — they simply give you nicer tools to build them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tutorials Teach the Hammer. Not When to Use It.
&lt;/h2&gt;

&lt;p&gt;Tutorials are exactly that: instructions on how to use a tool. They show you the toolbox.&lt;/p&gt;

&lt;p&gt;“Look,” they say, “here is the hammer. Here is the screwdriver. Here is the wrench. Here is the power drill.”&lt;br&gt;
Then they demonstrate.&lt;/p&gt;

&lt;p&gt;You use the hammer to hammer. Usually that happens when you have a nail. They might even show you a better way to hold the hammer, or how to hit the nail without smashing your thumb. All useful things.&lt;/p&gt;

&lt;p&gt;What they rarely discuss is whether you should be using the hammer at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the wall made of wood?
&lt;/li&gt;
&lt;li&gt;Is it concrete?
&lt;/li&gt;
&lt;li&gt;Is it safe to put nails there?
&lt;/li&gt;
&lt;li&gt;Is there already a pipe behind that wall?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yes, hammers can also be used to destroy things. But let’s not destroy that master wall.&lt;/p&gt;

&lt;p&gt;Tutorials are very good at showing how tools work. They are much less interested in explaining &lt;strong&gt;when those tools should exist in the first place&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you need to solve a very small problem, a tutorial is perfect. You follow the steps, you get a result, everyone is happy. But if you want to build a house, knowing how to swing a hammer is not enough.&lt;/p&gt;

&lt;p&gt;You need to understand foundations.&lt;/p&gt;




&lt;p&gt;Ideally, university or any other formal training would already have introduced many of these concepts.&lt;br&gt;
Dependency injection. Design patterns. Architecture principles. Separation of concerns.&lt;/p&gt;

&lt;p&gt;But let’s be honest: without some experience, these words often exist as concepts in a void. You read about them. You memorise definitions. You might even pass an exam about them.&lt;/p&gt;

&lt;p&gt;And then you start working on real systems and realise you have no idea what any of it actually looks like in practice. &lt;br&gt;
Because understanding usually comes in a different order.&lt;/p&gt;

&lt;p&gt;First you experiment. You write code. You break things. You copy examples. You follow tutorials. You try frameworks. You build small things that work — and sometimes things that really shouldn’t.&lt;/p&gt;

&lt;p&gt;At that stage learning is messy and unstructured. And that’s fine. In fact, it is often necessary.&lt;/p&gt;

&lt;p&gt;And that’s fine. In fact, it is often necessary. You see the same problems. The same shapes in the code. The same attempts to organise behaviour. And then the foundations become important again.&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s the Same Idea Wearing a Spring Jacket
&lt;/h2&gt;

&lt;p&gt;If you understand what &lt;strong&gt;Dependency Injection&lt;/strong&gt; and &lt;strong&gt;Inversion of Control&lt;/strong&gt; are, then Spring Boot's &lt;code&gt;@Autowired&lt;/code&gt; is not mysterious at all.&lt;/p&gt;

&lt;p&gt;The same idea appears everywhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Inject&lt;/code&gt; in Java EE or Quarkus.
&lt;/li&gt;
&lt;li&gt;The DI container in C# with .NET.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different frameworks, same principle.&lt;br&gt;
The framework did not invent the idea. It simply packaged it.&lt;br&gt;
The same happens with other things you will encounter in the Spring ecosystem.&lt;/p&gt;

&lt;p&gt;Once you understand &lt;strong&gt;Decorator&lt;/strong&gt; and &lt;strong&gt;Proxy&lt;/strong&gt; patterns, annotations like &lt;code&gt;@Transactional&lt;/code&gt; or &lt;code&gt;@Async&lt;/code&gt; become much easier to reason about. They behave like decorators: they add behaviour around a method call. Spring simply implements that layering with proxies and interception instead of manual wrapper objects.&lt;/p&gt;

&lt;p&gt;"&lt;code&gt;HandlerInterceptor&lt;/code&gt; and filters?": That is just &lt;strong&gt;Chain of Responsibility&lt;/strong&gt; wearing a Spring jacket.&lt;/p&gt;

&lt;p&gt;So yes — if you are going to work with Spring Boot, you should absolutely learn Spring Boot. But it becomes &lt;strong&gt;much easier to learn&lt;/strong&gt; when the foundations are already in place. And those foundations also make it much easier to move between frameworks later.&lt;/p&gt;

&lt;p&gt;Because once you know what problem you are trying to solve, the question becomes simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I do this here?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You need native queries? You understand the trade-offs, so you search: &lt;em&gt;how do I do this in Spring Boot?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You want retries because a service call might fail? You understand the use case, so you look at the multiple ways Spring provides retries — and there are many. &lt;br&gt;
Which is another reason why foundations matter: ecosystems tend to offer &lt;strong&gt;a lot of ways&lt;/strong&gt; to do the same thing.&lt;/p&gt;

&lt;p&gt;And then there is unit testing. You'll ask how to solve that with SpringBoot and someone will inevitably say:  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's use &lt;code&gt;@SpringBootTest&lt;/code&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ah! Trick question. You &lt;strong&gt;usually&lt;/strong&gt; don't use Spring Boot for &lt;strong&gt;unit tests&lt;/strong&gt;.  &lt;code&gt;@SpringBootTest&lt;/code&gt; is for component / integration tests. &lt;/p&gt;

&lt;p&gt;But there are also Test Slices, which, are indeed, mainly a Spring Boot concept, but you don’t need to worry about them until you understand what kind of tests you actually want to write.&lt;/p&gt;

&lt;p&gt;See? Foundations.&lt;/p&gt;

&lt;p&gt;Worst case scenario: you discover the framework does not actually solve that particular problem for you. Then you reach for a library — or you implement it yourself. But because you understand the concepts, you can still do it.&lt;/p&gt;




&lt;p&gt;If you want a place to start building those foundations, &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring Guru&lt;/strong&gt; is excellent for learning design patterns.&lt;/li&gt;
&lt;li&gt;Venkat Subramaniam is a fantastic speaker for explaining concepts clearly.
&lt;/li&gt;
&lt;li&gt;The Java YouTube channel is great to keep up with the evolution of the platform.
&lt;/li&gt;
&lt;li&gt;Some of the classics are still worth reading — Martin Fowler, Robert C. Martin, Sam Newman.
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Fundamentals of Software Architecture&lt;/em&gt; is also a great book.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the Spring ecosystem itself has some excellent educators as well. Josh Long and the Spring team often explain not just &lt;em&gt;how to use the tool&lt;/em&gt;, but the ideas behind it. Which is the important part.&lt;/p&gt;

&lt;p&gt;And yes, tools like Claude, ChatGPT, Gemini and friends can also be very helpful for learning these concepts — &lt;strong&gt;as long as you know how to ask the question&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But that is also the catch. Knowing how to phrase the question usually means you already understand the foundations you are asking about. Which is why I still tend to recommend the other sources first.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>software</category>
      <category>career</category>
      <category>java</category>
    </item>
    <item>
      <title>We Built a Wall Between Dev and Ops</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Fri, 27 Feb 2026 22:24:54 +0000</pubDate>
      <link>https://forem.com/srsandrade/we-built-a-wall-between-dev-and-ops-2741</link>
      <guid>https://forem.com/srsandrade/we-built-a-wall-between-dev-and-ops-2741</guid>
      <description>&lt;h2&gt;
  
  
  Two Villages and a Very Professional Wall
&lt;/h2&gt;

&lt;p&gt;There were once two villages in a valley.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On one side lived the Developers.&lt;/strong&gt;&lt;br&gt;
They wrote code. They shipped features. They believed progress was measured in commits per hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the other side lived the Ops tribe.&lt;/strong&gt;&lt;br&gt;
They guarded uptime. They feared outages. They believed progress was measured in how little changed.&lt;/p&gt;

&lt;p&gt;They were not enemies. But they were not friends either.&lt;/p&gt;

&lt;p&gt;The Developers would build something magnificent and launch it over the wall with confidence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“It works on my machine!”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Ops tribe would catch it, stare at it quietly, and reply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“That is… concerning.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Between the villages stood a large wall. A very capable wall.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wall had departments.&lt;/li&gt;
&lt;li&gt;The wall had KPIs.&lt;/li&gt;
&lt;li&gt;The wall had separate leadership, separate objectives, and separate bonus structures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers were rewarded for speed. Ops were rewarded for stability. These goals rarely shook hands.&lt;/p&gt;

&lt;p&gt;At the top of the wall, the chiefs sometimes met:&lt;br&gt;
Chief Trumpur of Delivery and Chief Ronald of Infrastructure.&lt;/p&gt;

&lt;p&gt;They would nod seriously and discuss improvements.&lt;/p&gt;

&lt;p&gt;Occasionally someone proposed making the wall bigger. &lt;br&gt;
“Just to clarify responsibilities.”&lt;br&gt;
And so more tickets were added; more processes; more approvals.&lt;/p&gt;

&lt;p&gt;Sometimes one tribe suggested the other should pay for it. This arrangement was considered professional.&lt;/p&gt;

&lt;p&gt;Inside each village, unity was also… aspirational.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Among Developers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Backend Guild spoke in APIs and schemas.&lt;/li&gt;
&lt;li&gt;The Frontend Circle debated pixels and state.
They met frequently for “alignment” and usually left slightly less aligned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Across the valley, Ops had its own clans:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudOps spoke in regions and availability zones.&lt;/li&gt;
&lt;li&gt;SysOps guarded servers like ancient relics.&lt;/li&gt;
&lt;li&gt;Database Administrators believed — not entirely unfairly — that everyone else was reckless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each group possessed sacred knowledge. Each quietly suspected the others did not understand how fragile everything truly was.&lt;/p&gt;

&lt;p&gt;Then one day, a prophet arrived. He spoke strange words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agile.&lt;/li&gt;
&lt;li&gt;Collaboration.&lt;/li&gt;
&lt;li&gt;Shared ownership.&lt;/li&gt;
&lt;li&gt;Continuous delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And most shocking of all:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why not remove the wall?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The villages gasped.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workshops were held.&lt;/li&gt;
&lt;li&gt;Slides were presented.&lt;/li&gt;
&lt;li&gt;Arrows pointed in both directions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, the wall fell. From its rubble emerged a new tribe:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DevOps.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was a beautiful name. A new era had begun. But demolition is easier than integration.&lt;br&gt;
The wall disappeared. The distance did not.&lt;/p&gt;

&lt;p&gt;People stayed where they had always stood — only now without bricks to blame.&lt;/p&gt;

&lt;p&gt;Questions replaced stone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who owns the pipeline?&lt;/li&gt;
&lt;li&gt;Who approves production access?&lt;/li&gt;
&lt;li&gt;Who responds at 3am?&lt;/li&gt;
&lt;li&gt;Who broke this?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Old Man Ronald would sometimes sigh:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“At least when there was a wall, we knew whose fault it was.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And slowly, quietly, new fences appeared. They had modern names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform Team&lt;/li&gt;
&lt;li&gt;SRE&lt;/li&gt;
&lt;li&gt;Infrastructure Service Team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The wall was gone. The instinct to rebuild it was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  DevOps as a Concept vs DevOps as a Renamed Silo
&lt;/h2&gt;

&lt;p&gt;Let’s leave the valley for a moment. A while ago, we were hiring.&lt;/p&gt;

&lt;p&gt;We needed people with experience in CI/CD, monitoring, Infrastructure as Code, and cloud environments. Not wizards. Just engineers who understood that software does not end at git push.&lt;/p&gt;

&lt;p&gt;What we found was… instructive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Candidate 1&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Had “CI/CD experience” on their CV.&lt;/li&gt;
&lt;li&gt;It turned out they had mostly worked with Ansible playbooks.&lt;/li&gt;
&lt;li&gt;They had never written application code.&lt;/li&gt;
&lt;li&gt;They did not know what a build lifecycle looked like.&lt;/li&gt;
&lt;li&gt;Cloud? No.&lt;/li&gt;
&lt;li&gt;Monitoring? Not really.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conclusion: In their previous company, they ran playbooks. That was the job. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Candidate 2&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listed themselves as “CloudOps – Mid Level.”&lt;/li&gt;
&lt;li&gt;When we asked what that meant, the answer was: “I’ve set up EC2 instances.” Which is fine.&lt;/li&gt;
&lt;li&gt;But that was the extent of it.&lt;/li&gt;
&lt;li&gt;No discussion of networking.&lt;/li&gt;
&lt;li&gt;No IAM.&lt;/li&gt;
&lt;li&gt;No scaling considerations.&lt;/li&gt;
&lt;li&gt;No CI/CD integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conclusion: Cloud meant “launching machines.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Candidate 3&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Had “DevOps Engineer” in bold.&lt;/li&gt;
&lt;li&gt;Impressive stack. Impressive terminology.&lt;/li&gt;
&lt;li&gt;After a few questions, it turned out their role was mostly maintaining a Jenkins instance someone else had designed years ago.&lt;/li&gt;
&lt;li&gt;They could restart it. They could update plugins.&lt;/li&gt;
&lt;li&gt;They had never written a pipeline from scratch.&lt;/li&gt;
&lt;li&gt;They had never debugged a failing deployment across environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conclusion: The pipeline worked. What it actually deployed was someone else’s concern.&lt;/p&gt;




&lt;p&gt;One thing common across all interviews:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Limited participation in development, little exposure to the product lifecycle, and sometimes only a surface-level understanding of the system itself.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;And this is not about mocking individuals. It is about what happens when we turn a philosophy into a job title.&lt;/p&gt;

&lt;p&gt;The mirror image exists on the development side. For many developers, infrastructure means writing a &lt;code&gt;docker-compose.yml&lt;/code&gt; file and hoping it requires zero understanding of networking, ports, certificates, or what actually happens once the containers leave their laptop.&lt;/p&gt;

&lt;p&gt;Abstraction is useful. Blindness is not.&lt;br&gt;
The original idea of DevOps was not to create a new role. It was to stop pretending development and operations were separate problems.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code decisions affect operations.&lt;br&gt;
Operational decisions affect code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Logging choices influence monitoring.&lt;/li&gt;
&lt;li&gt;Framework capabilities influence observability.&lt;/li&gt;
&lt;li&gt;Infrastructure shapes authentication, scaling, and resilience.&lt;/li&gt;
&lt;li&gt;Application design determines how deployments succeed — or fail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A developer does not need to become an infrastructure specialist.&lt;br&gt;
An operations engineer does not need to become a feature developer.&lt;/p&gt;

&lt;p&gt;But neither can be blind to the other side.&lt;/p&gt;

&lt;p&gt;Because there is no “other side.” The system is continuous.&lt;br&gt;
And yes — foundations matter.&lt;/p&gt;

&lt;p&gt;A developer should understand what a pipeline is doing when it runs.&lt;br&gt;
Not necessarily how to rebuild Jenkins from scratch, but what stages exist, why tests run where they run, what a deployment actually means, and what can go wrong between commit and production.&lt;br&gt;
And yes — sometimes that also means being able to tweak a Jenkinsfile, adjust a pipeline configuration, or make a small change to a CloudFormation template. Not as an infrastructure expert, but with enough understanding to know &lt;em&gt;what the file is for&lt;/em&gt;, what it controls, and why the change matters.&lt;/p&gt;

&lt;p&gt;Otherwise, code is written in a vacuum and surprises appear later — usually at 5pm on a Friday.&lt;/p&gt;

&lt;p&gt;The same applies in reverse. Operations should understand what is being deployed well enough to recognise when the platform is solving a problem that the application already solved — or could solve more simply.&lt;br&gt;
They should also understand what the application actually does. Not every line of code, but which modules are critical, which services carry business risk, and how configuration changes might affect behaviour. &lt;/p&gt;

&lt;p&gt;Infrastructure decisions make more sense when you understand the system running on top of it. Some modules deserve deeper monitoring because they are business-critical. Some alerts matter more than others. Some failures are noise; others stop revenue.&lt;/p&gt;

&lt;p&gt;You don’t need to be the person who wrote the feature.&lt;br&gt;&lt;br&gt;
But you do need enough context to understand what you are keeping alive.&lt;/p&gt;

&lt;p&gt;This is not about everyone knowing everything. It is about everyone knowing enough.&lt;/p&gt;

&lt;p&gt;Because without shared foundations, collaboration becomes ticket exchange.&lt;br&gt;
And ticket exchange was exactly the wall DevOps was supposed to remove.&lt;/p&gt;

&lt;h2&gt;
  
  
  DevOps (Now With New Paint)
&lt;/h2&gt;

&lt;p&gt;To be clear: I am not saying real DevOps does not exist.&lt;/p&gt;

&lt;p&gt;There are certainly companies where shared ownership actually happens. Where developers understand how their software behaves in production, and operations engineers understand the applications they support. Where pipelines, monitoring, deployments, and code evolve together instead of being negotiated through tickets.&lt;/p&gt;

&lt;p&gt;Those places exist. They are just… less common than the conference talks might suggest.&lt;/p&gt;

&lt;p&gt;And there &lt;em&gt;are&lt;/em&gt; engineers who naturally work this way — people who move comfortably across domains, ask questions outside their specialty, and try to understand systems as a whole instead of protecting a narrow slice of responsibility.&lt;/p&gt;

&lt;p&gt;This is not a claim that DevOps failed everywhere. It’s a claim that many organisations kept the exact same structure… and simply changed the name.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wall stayed.&lt;/li&gt;
&lt;li&gt;The tickets stayed.&lt;/li&gt;
&lt;li&gt;The handoffs stayed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the org chart changed. What used to be &lt;em&gt;Development&lt;/em&gt; and &lt;em&gt;Operations&lt;/em&gt; became something else but...new name, same queue.&lt;/p&gt;

&lt;p&gt;And honestly, I don’t particularly care what we call it. Call it DevOps. Call it Platform Engineering. Call it Deployment Happiness Engineering if that helps morale.&lt;/p&gt;

&lt;p&gt;But if work still crosses organisational borders as requests instead of shared responsibility, then the wall is still there — just painted a different colour.&lt;/p&gt;

&lt;p&gt;And if the plan now is “the AI will handle it,” just remember: AI outputs are just your inputs… with confidence added.&lt;/p&gt;

&lt;p&gt;In conclusion: now everyone agrees the wall is gone… while quietly adding new bricks: more ownership boundaries, more approval layers, more specialised silos.&lt;/p&gt;

&lt;p&gt;We demolished the wall in theory. Then rebuilt it in Jira.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>discuss</category>
      <category>management</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Day the AI Took My Requirements Literally</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Fri, 20 Feb 2026 00:22:33 +0000</pubDate>
      <link>https://forem.com/srsandrade/the-day-the-ai-took-my-requirements-literally-24a1</link>
      <guid>https://forem.com/srsandrade/the-day-the-ai-took-my-requirements-literally-24a1</guid>
      <description>&lt;h2&gt;
  
  
  The AI Built Exactly What Was Asked For. That Was the Problem.
&lt;/h2&gt;

&lt;p&gt;A new project was starting.&lt;/p&gt;

&lt;p&gt;The proposal described a distributed, multi-domain system responsible for processing financial transactions, aggregating operational metrics, and exposing analytical insights to multiple internal stakeholders.&lt;/p&gt;

&lt;p&gt;There were compliance considerations. There were integration points with legacy services. There were performance expectations. There was the word “real-time” in bold.&lt;/p&gt;

&lt;p&gt;The requirements included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An event-driven real-time streaming pipeline.&lt;/li&gt;
&lt;li&gt;CQRS with event sourcing for future scalability.&lt;/li&gt;
&lt;li&gt;A service mesh for observability and traffic control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It sounded serious. It sounded modern. It sounded like something you would proudly present in a boardroom.&lt;br&gt;
The company had recently rolled out its new AI-enabled development infrastructure. Internal agents. Architecture generators. Prompt templates. “Accelerated delivery pipelines.”&lt;/p&gt;

&lt;p&gt;The team fed the requirements into it.&lt;br&gt;
The AI did not hesitate.&lt;/p&gt;

&lt;p&gt;An architecture emerged.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Message brokers coordinating streams of domain events.
&lt;/li&gt;
&lt;li&gt;Dedicated command and query models backed by separate data stores.
&lt;/li&gt;
&lt;li&gt;Event sourcing to maintain an immutable audit trail.
&lt;/li&gt;
&lt;li&gt;Sidecars injected for traffic control.
&lt;/li&gt;
&lt;li&gt;mTLS between services.
&lt;/li&gt;
&lt;li&gt;Retries. Circuit breakers. Distributed tracing.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was the best system. So robust, so scalable. The best enterprise application.&lt;br&gt;
And then a preliminary AWS estimate arrived two days later. Projected monthly infrastructure cost: &lt;strong&gt;$42,300&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That included managed streaming clusters, multi-AZ databases for command and query models, service mesh overhead, observability tooling, and three separate environments.&lt;/p&gt;

&lt;p&gt;But there were no hallucinations. No obvious mistakes.&lt;br&gt;
Just a perfectly coherent interpretation of the requirements. The AI built exactly what was asked for.&lt;/p&gt;

&lt;p&gt;That was the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Garbage In. AI Out. Repeat. Or: How We Professionally Automated Our Own Confusion
&lt;/h2&gt;

&lt;p&gt;Let’s rewind.&lt;br&gt;
Before the architecture. Before the AWS estimate. Before the “enterprise-grade” diagram.&lt;/p&gt;

&lt;p&gt;There was a stakeholder. Stakeholder wanted to build an internal operations platform to monitor transaction processing and generate insights for management.&lt;/p&gt;

&lt;p&gt;Reasonable goal. They opened ChatGPT.&lt;/p&gt;

&lt;p&gt;They typed something like:&lt;br&gt;
&lt;code&gt;What architecture should I use to build a scalable, real-time financial monitoring platform that might grow in the future?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;ChatGPT did what it does best. It delivered ambition. The model mentioned event-driven architecture, streaming pipelines and service meshes for observability and control!&lt;/p&gt;

&lt;p&gt;“Service mesh,” the stakeholder thought.&lt;br&gt;&lt;br&gt;
That sounds important.&lt;/p&gt;

&lt;p&gt;They type a follow-up question:&lt;br&gt;
&lt;code&gt;How do we make sure the system scales well if usage grows?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;ChatGPT responded confidently:&lt;br&gt;
&lt;code&gt;Modern systems often adopt patterns such as CQRS and event sourcing to separate concerns, improve scalability, and support future growth.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;CQRS. The stakeholder had heard that word before. Someone from engineering had mentioned it in a meeting once.&lt;br&gt;
It sounded serious, modern and safe. &lt;/p&gt;

&lt;p&gt;So they asked:&lt;br&gt;
&lt;code&gt;Would CQRS make our platform more future-proof?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;ChatGPT, still helpful:&lt;br&gt;
&lt;code&gt;Yes, CQRS is commonly used in systems that anticipate growth and evolving requirements.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Future-proof. There it was again. And it sounded very reassuring.&lt;/p&gt;

&lt;p&gt;So the stakeholder wrote a very serious proposal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It contained “real-time streaming architecture.”
&lt;/li&gt;
&lt;li&gt;It contained “CQRS with event sourcing.”
&lt;/li&gt;
&lt;li&gt;It contained “service mesh for resiliency and governance.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The requirements were vague. The ambition was abstract. The context was thin.&lt;/p&gt;

&lt;p&gt;Garbage in. → AI out.&lt;br&gt;
Then that proposal was fed into another AI-powered system.&lt;/p&gt;

&lt;p&gt;And it produced a perfectly consistent, technically correct, financially enthusiastic architecture.&lt;/p&gt;

&lt;p&gt;Garbage in. → AI out. → Repeat.&lt;/p&gt;




&lt;p&gt;By the time it reached the architecture review, the proposal sounded heavy. It looked substantial.&lt;br&gt;
What no one had written clearly was this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There were 25 users. At most. &lt;/li&gt;
&lt;li&gt;“Real-time” meant “an update every hour or so.”&lt;/li&gt;
&lt;li&gt;The “legacy integration” was a REST API. Poorly documented, yes... but still a REST API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The expected traffic curve could comfortably fit inside a single moderately-sized instance without breaking a sweat.&lt;/p&gt;

&lt;p&gt;The vague ambition travelled further than the concrete constraints. And the AI did exactly what it was asked to do.&lt;/p&gt;

&lt;p&gt;It optimised for the bold words, for growth, for the future. It was not, however, optimised for reality. Of course, this is a slightly exaggerated example.&lt;/p&gt;

&lt;p&gt;But only slightly. I have seen real systems where the architecture slides were more complex than any business case.&lt;br&gt;
And to be fair, the opposite happens too.&lt;/p&gt;

&lt;p&gt;Sometimes the requirement arrives as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“It’s just adding an &lt;code&gt;if&lt;/code&gt; to the tool we already have.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Just an &lt;code&gt;if&lt;/code&gt;. Behind that “if”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;18,000 lines of legal compliance requirements.&lt;/li&gt;
&lt;li&gt;Regional regulatory variations.&lt;/li&gt;
&lt;li&gt;Audit trail obligations.&lt;/li&gt;
&lt;li&gt;Data retention constraints.&lt;/li&gt;
&lt;li&gt;Twenty bespoke hardware integrations with devices that were configured in 2014 by someone who no longer works here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the proposal says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Minor enhancement.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI will happily believe that too.&lt;br&gt;
It will produce a neat solution for a simple conditional branch.&lt;/p&gt;

&lt;p&gt;Again, this is slightly exaggerated. But (again) only slightly. I have genuinely heard someone say, “It’s just an &lt;code&gt;if&lt;/code&gt;”,  for a problem that required eight scalable AWS services, ingestion from thousands of devices and near real-time reporting,.&lt;/p&gt;

&lt;p&gt;It was not, in fact, just an &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Garbage in → garbage out has always been true. Confusing requirements did not start with AI.&lt;/p&gt;

&lt;p&gt;The difference now is speed and confidence.&lt;/p&gt;

&lt;p&gt;We used to miscommunicate slowly. A developer would question it. A meeting would happen. Someone would sigh. Clarifications would emerge.&lt;/p&gt;

&lt;p&gt;Now we can take vague ambition, feed it into a model, generate a polished architecture, feed that into another system, and deploy it way before anyone asks whether the original requirement made sense.&lt;/p&gt;

&lt;p&gt;Garbage in → AI out → Feed that output back in →  Repeat.&lt;br&gt;
Confusion used to stay human-sized. Now it scales.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which brings us to the uncomfortable part.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Prompt Engineering Is Just (Mis)Communication With Better Marketing
&lt;/h2&gt;

&lt;p&gt;We have been trying to fix human miscommunication for years.&lt;br&gt;
User stories. BDD. Acceptance criteria. Refinement meetings. Specification templates. Diagrams.&lt;br&gt;
Workshops about workshops.&lt;/p&gt;

&lt;p&gt;Entire methodologies exist because humans are terrible at saying what they want.&lt;/p&gt;

&lt;p&gt;And if anything has been proven, it’s that we remain terrible at communicating and writing.&lt;/p&gt;

&lt;p&gt;Now we have “prompt engineering” — which sounds very fancy and technical — but is essentially communicating by text with a very confident, opinionated rubber duck.&lt;/p&gt;

&lt;p&gt;That, by itself, will solve nothing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We say “scalable” and mean “won’t crash.”&lt;br&gt;
We say “real-time” and mean “doesn’t feel slow.”&lt;br&gt;
We say “future-proof” and mean “I don’t want to revisit this.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We assume everyone shares the same mental model. And they don't.&lt;br&gt;
AI does not fix this. It removes the human buffer. &lt;/p&gt;

&lt;p&gt;There is also the small detail of language. The same prompt written in English, Spanish, or German will not always produce the same output. Nuance shifts. Assumptions shift. Tone shifts.&lt;br&gt;
If “lost in translation” is a problem between humans, it does not disappear with a probabilistic model. It just becomes statistically interesting.&lt;/p&gt;

&lt;p&gt;Ambiguity is often resolved by questioning. A lot of questioning.&lt;br&gt;
The human (ideally) will ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Do we actually need real-time?”&lt;/li&gt;
&lt;li&gt;“Is this a CQRS problem or a CRUD problem?”&lt;/li&gt;
&lt;li&gt;“Do we have enough services to justify a service mesh?”&lt;/li&gt;
&lt;li&gt;“What’s the traffic?”&lt;/li&gt;
&lt;li&gt;“What’s the budget?”&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Yes. Never trust the requirements on their own. Requirements are optimistic by design.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI, however, trusts the requirements.&lt;/p&gt;

&lt;p&gt;If you write:&lt;br&gt;
&lt;code&gt;Let’s build an event-driven real-time streaming pipeline.&lt;/code&gt;&lt;br&gt;
It builds one.&lt;/p&gt;

&lt;p&gt;If you write:&lt;br&gt;
&lt;code&gt;Let’s use CQRS with event sourcing to future-proof.&lt;/code&gt;&lt;br&gt;
It splits your system in two and prepares you for scale you may never reach.&lt;/p&gt;

&lt;p&gt;If you write:&lt;br&gt;
&lt;code&gt;We should introduce a service mesh for observability and control&lt;/code&gt;&lt;br&gt;
It configures sidecars, mTLS, traffic policies, retries. &lt;/p&gt;

&lt;p&gt;The AI will trust your ambition and will not even ask if you have the budget to match it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stakeholders sometimes bring big technical words. They do not always bring matching “big” money.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s be clear: I am very glad AI does not fight back.&lt;br&gt;
I am not prepared to argue with the AI overlords — or with a chatbot that has developed ego.&lt;br&gt;
But someone should. (Fight back, I mean.)&lt;/p&gt;

&lt;p&gt;Because AI does not measure necessity. It measures alignment.&lt;/p&gt;

&lt;p&gt;It will scale like the users exist.&lt;br&gt;
It will future-proof like the roadmap exists.&lt;br&gt;
It will architect like the money exists.&lt;/p&gt;

&lt;p&gt;Prompt engineering is not magic. It is structured communication without interruption.&lt;/p&gt;

&lt;p&gt;And if we were imprecise before, we are now imprecise with acceleration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Human in the Loop, Not AI in the Loop
&lt;/h2&gt;

&lt;p&gt;Let’s un-exaggerate this for a second and gently deflate the enterprise dreams: all the previous examples were somewhat optimistic about the capabilities of AI. &lt;br&gt;
I have yet to see an AI model or agent that can take a full-blown PDF specification, translate it into a complete system, preserve every nuance, and not quietly drop 15% of the context somewhere between page 12 and Annex C.&lt;/p&gt;

&lt;p&gt;I have tried something simpler:&lt;br&gt;&lt;br&gt;
&lt;code&gt;“Here are two versions of a specification as PDFs. Tell me the differences.”&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What I usually get back is enthusiasm and vibes, instead of a traceable list of changes. A few of the vibes are sometimes correct.&lt;/p&gt;

&lt;p&gt;Which means I cannot trust it blindly. Which means I have to read the specification myself anyway — to confirm that the AI didn’t miss half of it or hallucinate a requirement that never existed.&lt;br&gt;
Which means… I still have to do the work.&lt;/p&gt;

&lt;p&gt;Now let’s imagine a better-case scenario.&lt;br&gt;
Let’s imagine the AI &lt;em&gt;does&lt;/em&gt; understand the specification perfectly and generates all the required code. I still need to review it.&lt;/p&gt;

&lt;p&gt;Line by line; behaviour by behaviour; edge case by edge case.&lt;/p&gt;

&lt;p&gt;If something doesn’t match expectations, I now have two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tune the prompts, refine the context, restructure the input, clarify assumptions… and try again
&lt;/li&gt;
&lt;li&gt;or just redo it myself — or delegate it to someone who will (we still might use AI to help in smaller increments).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And there is another subtle problem. AI depends heavily on familiarity. Imagine you are working with Java 25. If most publicly available examples, blog posts, and Stack Overflow discussions revolve around Java 8, then statistically speaking, that is the world the model understands best.&lt;br&gt;
You can explicitly ask for Java 25. It will try. But models gravitate toward what they have seen most often.  So now your review job expands again.&lt;/p&gt;

&lt;p&gt;And even in the magical scenario where the AI produces flawless, syntactically perfect, technically coherent and version adherent code — there is still the one uncomfortable truth that initiated this whole article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the human requirements were vague, confused, or contradictory…&lt;br&gt;&lt;br&gt;
the output will be vague, confused, and contradictory — just very efficiently so.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And no model can compensate for missing intent.&lt;/p&gt;




&lt;p&gt;Now, before this turns into “AI is useless,” let me be clear: I am not against AI. Quite the opposite.&lt;/p&gt;

&lt;p&gt;AI is extremely useful when it is positioned correctly. You probably shouldn’t let it write your documentation from scratch.&lt;/p&gt;

&lt;p&gt;But you absolutely can ask it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tighten your writing,&lt;/li&gt;
&lt;li&gt;improve clarity,&lt;/li&gt;
&lt;li&gt;suggest structure,&lt;/li&gt;
&lt;li&gt;highlight ambiguities,&lt;/li&gt;
&lt;li&gt;spot inconsistencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It will save you hours of re-reading your own text and wondering why that paragraph “feels off.”&lt;br&gt;
You probably shouldn’t let it replace code reviews entirely.&lt;/p&gt;

&lt;p&gt;But you &lt;em&gt;can&lt;/em&gt; use tools that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;flag suspicious logic,&lt;/li&gt;
&lt;li&gt;detect edge cases,&lt;/li&gt;
&lt;li&gt;suggest refactors,&lt;/li&gt;
&lt;li&gt;answer follow-up questions in pull request threads,&lt;/li&gt;
&lt;li&gt;tell you that according to the version of the tool that you are using, there are simpler ways to do something.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Worst case?&lt;br&gt;&lt;br&gt;
You dismiss the comment — just like you would if a colleague misunderstood the context.&lt;/p&gt;

&lt;p&gt;Examples of other areas where it shines, if expectations are realistic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Suggesting boundary conditions, or pointing out “Have you considered null here?” moments. You still decide what matters. It just saves typing and forgetfulness.&lt;/li&gt;
&lt;li&gt;Generating repetitive scaffolding: templates, boilerplate, basic CRUD layers, migration scripts. The kind of code that is necessary but not intellectually exciting. You still review it. You still adapt it. But you type less.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all these cases, AI is not replacing the human. It is assisting the human.&lt;br&gt;
That’s the difference. When you put &lt;strong&gt;AI in the loop&lt;/strong&gt;, you risk removing accountability and context.&lt;/p&gt;

&lt;p&gt;AI will happily suggest the architecture but &lt;strong&gt;it will not&lt;/strong&gt; sit in the budget review defending it. And, a person walking into that meeting and saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Well… the AI model told me to.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…is not exactly a career-enhancing move.&lt;/p&gt;

&lt;p&gt;When you keep &lt;strong&gt;humans in the loop&lt;/strong&gt;, AI becomes leverage.&lt;br&gt;
Every system has loops.&lt;br&gt;
Requirements go in. → Architectures come out. → Costs follow.&lt;/p&gt;

&lt;p&gt;AI can sit inside that loop. But it cannot, or at least should not, own the loop. AI should not be a replacement for thinking.&lt;br&gt;
If you let that happen confusion will scale better than your system ever will. And the loop becomes something else entirely:&lt;/p&gt;

&lt;p&gt;Garbage in. → AI out. → Repeat.&lt;/p&gt;

&lt;p&gt;Faster each time.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>software</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Jack VS the AI Machine</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Fri, 13 Feb 2026 20:53:35 +0000</pubDate>
      <link>https://forem.com/srsandrade/jack-vs-the-ai-machine-1o1l</link>
      <guid>https://forem.com/srsandrade/jack-vs-the-ai-machine-1o1l</guid>
      <description>&lt;h2&gt;
  
  
  The Specialist and the Box
&lt;/h2&gt;

&lt;p&gt;There once was a developer who knew Kubernetes. Let’s call him Bruno.&lt;/p&gt;

&lt;p&gt;Not “has deployed something once.” Not “can read a tutorial.”&lt;/p&gt;

&lt;p&gt;Bruno knew it. If there was a question about Kubernetes, Bruno was already answering it. If a project involved Kubernetes, there would inevitably be a moment where Bruno stood in front of a diagram explaining how everything worked. When something behaved strangely, everyone turned to Bruno.&lt;/p&gt;

&lt;p&gt;And slowly, without anyone announcing it, a box formed.&lt;/p&gt;

&lt;p&gt;The box had boundaries: containers, clusters, networking, scaling policies, resource limits. Inside those boundaries, Bruno was fluent. He knew the failure modes. He knew the edge cases. Outside the box… that was someone else’s diagram.&lt;/p&gt;

&lt;p&gt;He no longer needed to be fully allocated to projects. He floated. He was invited when “scaling” appeared in a document.&lt;/p&gt;

&lt;p&gt;Eventually, the company made it official. Bruno became the &lt;strong&gt;Kubernetes Titan&lt;/strong&gt;. It appeared in org charts, in email signatures, and at least in one PowerPoint template. The box now had a name.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inside the Box, Life Is Good
&lt;/h2&gt;

&lt;p&gt;Life is good inside the box.&lt;/p&gt;

&lt;p&gt;Bruno does not attend daily stand-ups. He does not debug pipelines. He is summoned when the topic is “orchestration.” He writes the guidelines: platform standards, deployment principles, slides titled “Kubernetes Strategy 2025.” He no longer reviews YAML — that is project work. Bruno defines what “good YAML” means.&lt;/p&gt;

&lt;p&gt;If something doesn’t fit the standards, the answer is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application must adapt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After all, the platform is correct. If something fails, it must be elsewhere. Inside the box, responsibility has edges. Bruno owns the platform. Everything else belongs to “the product.” Life is clean when responsibility is clean.&lt;/p&gt;

&lt;p&gt;Lately, though, the questions feel different. They are not about scaling thresholds or deployment patterns. They are about how everything fits together.&lt;/p&gt;

&lt;p&gt;Those are not Kubernetes questions.&lt;/p&gt;

&lt;p&gt;Bruno prefers Kubernetes questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Machine Eats the Box
&lt;/h2&gt;

&lt;p&gt;At first, Bruno doesn’t notice. Then he does.&lt;/p&gt;

&lt;p&gt;The meetings are shorter. Fewer architecture reviews. Fewer “Can we align on this?” messages. People still use Kubernetes — they just don’t need Bruno to explain it.&lt;/p&gt;

&lt;p&gt;There is a new habit in the company. Logs go into chat windows. Diagrams go into chat windows. Entire systems get described to agents that reply in seconds. Instead of asking Bruno, they ask the machine.&lt;br&gt;
No meeting required.&lt;/p&gt;

&lt;p&gt;The machine answers quickly — deployment structures, scaling rules, example configurations. The answers are not perfect. But they are fast. And often… good enough.&lt;/p&gt;

&lt;p&gt;Bruno tries it too. When the question stays inside the box, he can judge the answer. He can correct it. But when the question crosses into application design, database structure, integration flows…&lt;/p&gt;

&lt;p&gt;Bruno reads the answer and does not know if it is right.&lt;/p&gt;

&lt;p&gt;He knows Kubernetes. He does not know the system.&lt;/p&gt;

&lt;p&gt;For the first time, that difference matters.&lt;/p&gt;

&lt;p&gt;Jack notices this sooner. Jack has been using the machine for weeks — not for Kubernetes, for everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Jack Knows Where to Click
&lt;/h2&gt;

&lt;p&gt;Jack works in the same company.&lt;/p&gt;

&lt;p&gt;He is not a Titan. Not a Wizard. Not a Pro Whisperer. Jack just… works on things. He has written Java, Python and Typescript, built Docker images, fixed pipelines, written tests, removed tests, shipped things.&lt;/p&gt;

&lt;p&gt;He does not know Kubernetes like Bruno does. But he understands the core ideas. And he understands something else: Kubernetes is powerful. It is also sometimes a cannon. And occasionally we are hunting flies.&lt;/p&gt;

&lt;p&gt;When Jack works on an application, he tries to understand what it is supposed to achieve — not just how it runs, but why it exists. He understands what the data represents, what the business expects to see, what monitoring should detect, what the pipeline guarantees. He reads the requirements — sometimes twice.&lt;/p&gt;

&lt;p&gt;Yes, Jack is kind of a bore. He has opinions about traceability.&lt;/p&gt;

&lt;p&gt;He ties things together: orchestration, data, business rules, monitoring, pipelines, documentation.&lt;/p&gt;

&lt;p&gt;When the machine produces an answer, Jack does not treat it as truth. He treats it as material. He knows which constraints matter and which assumptions cannot break. He doesn’t ask for a solution in isolation. He gives the machine the context — and asks for something that fits.&lt;/p&gt;

&lt;p&gt;And when the machine answers confidently, Jack reads carefully. Sometimes he can’t explain why something feels off. But he can tell. And when he cannot tell, he knows where to look.&lt;/p&gt;

&lt;p&gt;He doesn’t need to master one box.&lt;/p&gt;

&lt;p&gt;He needs to move between them — and recognise when the machine is guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Jack Has the Big Red Button
&lt;/h2&gt;

&lt;p&gt;Let’s drop the story for a moment.&lt;/p&gt;

&lt;p&gt;Kubernetes is just an example. This isn’t about Kubernetes. It isn’t about specialists being useless. It’s about boxes — and what happens when we stay inside them too long.&lt;/p&gt;

&lt;p&gt;There’s a lot of noise about AI replacing juniors. Maybe. But there’s another group quietly at risk: the specialist who lives entirely inside one box.&lt;/p&gt;

&lt;p&gt;AI does not just generate code. It connects things. It drafts architecture. It crosses boundaries without asking for permission.&lt;/p&gt;

&lt;p&gt;If you only understand one layer deeply — but not how it interacts with others — you struggle differently. You don’t know what to ask. You don’t know what context matters. You don’t know whether the answer is subtly wrong.&lt;/p&gt;

&lt;p&gt;This didn’t start with AI. The industry has been accelerating for years. Safe niches become narrow corridors.&lt;/p&gt;

&lt;p&gt;The generalist — the so-called “jack of all trades” — never had the comfort of one box. They had to adapt. And AI rewards adaptation. It rewards context. It rewards people who can see across layers.&lt;/p&gt;

&lt;p&gt;For years, I have been skeptical of rigid separation: developers here, validators there, DevOps in another silo. Those structures often create friction, duplicated effort, and systems that feel stitched together rather than designed. With AI in the mix, those walls become even more problematic.&lt;/p&gt;

&lt;p&gt;If the machine sees the whole system, but the engineers only see their slice, the machine will move faster than the humans.&lt;br&gt;
The generalist isn’t powerful because they know everything. They are powerful because they connect things. And the person who connects things decides what gets built.&lt;/p&gt;

&lt;p&gt;The machine can generate. The machine can connect.&lt;/p&gt;

&lt;p&gt;But someone still has to decide what makes sense.&lt;/p&gt;

&lt;p&gt;That’s the button.&lt;br&gt;
Jack presses it.&lt;br&gt;
Carefully.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>My Data Lake Runs on MongoDB and PostgreSQL and I’m Not Sorry</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Sat, 07 Feb 2026 13:34:10 +0000</pubDate>
      <link>https://forem.com/srsandrade/my-data-lake-runs-on-mongodb-and-postgresql-and-im-not-sorry-1g2j</link>
      <guid>https://forem.com/srsandrade/my-data-lake-runs-on-mongodb-and-postgresql-and-im-not-sorry-1g2j</guid>
      <description>&lt;h2&gt;
  
  
  A Brief History of How I Angered Absolutely No One (So Far)
&lt;/h2&gt;

&lt;p&gt;Before we ever talked about technologies, we (a team working in a project) were dealing with a fairly uncomfortable data problem. We were collecting large volumes of data from multiple external sources, many of which we did not fully control or even fully understand. At the point of collection, the data arrived with little reliable context, inconsistent structure, and no clear guarantees about meaning.&lt;/p&gt;

&lt;p&gt;More importantly, the data could not be meaningfully analysed at ingestion time. Individual values were not inherently valid or invalid, useful or useless. Their significance only emerged later, once they were combined with other datasets, configuration, and a set of calculations applied in a subsequent processing phase.&lt;/p&gt;

&lt;p&gt;To make this harder, the same incoming data could eventually belong to different processing “plans”. Each plan defined its own expectations around data types, granularity, frequency, and validation rules. New plans could appear over time, existing ones could evolve, and at the moment the data was collected there was no reliable way to know which plan would ultimately apply.&lt;/p&gt;

&lt;p&gt;There was also a strict traceability requirement: any calculation or recalculation had to be fully attributable to the original input values. This meant that once data was ingested, the values themselves could not be updated or rewritten. Corrections and reinterpretations had to be expressed as new processing steps, not mutations of the original records.&lt;/p&gt;

&lt;p&gt;Faced with this, the textbook answer is a data lake. A “proper” data lake, at least in theory, is a place where raw data can be stored cheaply and indefinitely, in its original form, without forcing early decisions about structure or schema. The promise is simple: ingest first, understand later.&lt;/p&gt;

&lt;p&gt;In practice, this usually means object storage at the core: large, inexpensive buckets where data is written once and rarely touched directly. Data is organised by conventions rather than rigid models, and meaning is derived at read time using analytical engines rather than enforced at write time. Around this storage layer sits an ecosystem of supporting technology - distributed query engines, batch processing frameworks, metadata catalogues, and orchestration tools - while the lake itself remains deliberately passive.&lt;/p&gt;

&lt;p&gt;On paper, this fits our problem almost perfectly. We had uncertain data, evolving interpretation rules, and no safe way to apply strict schemas up front. Deferring meaning was a requirement.&lt;/p&gt;

&lt;p&gt;And yet, this is also where the theory starts to fray. Our problem was not just storing raw data for later analytics; it was needing to interact with that raw data operationally, correct it, investigate it, and understand it while the system was running. The data lake model explains where to put uncertain data, but it says much less about how to live with it day to day.&lt;/p&gt;

&lt;p&gt;So the question stopped being “where do we dump raw data cheaply?” and became “how do we live with raw data every day?”. We needed to inspect and query raw data operationally, without introducing extra ETL processes which would exist only to make the data usable, we needed to correlate records to plans once the missing context became available, and we needed to support corrections and investigations without rewriting history. The point was to keep immutability of original values for traceability, while still being able to attach new metadata and interpretations close to the original information - not as a separate universe that requires rebuilding pipelines every time we learn something new.&lt;/p&gt;

&lt;p&gt;In a classic data lake stack, that usually means object storage plus a surrounding ecosystem: file formats like Parquet or Avro; table layers such as Iceberg, Delta, or Hudi; catalogues like Glue, Hive, or Unity; batch processing engines like Spark or Flink; and interactive query engines such as Trino or Presto, all held together by orchestration on top. That world is powerful, but it also tends to move complexity into “process”: jobs, compaction, reprocessing, schema evolution, and the operational overhead of making raw data convenient to interrogate. Cheap storage is great, but we also cared about cheap processes and cheap debugging - and getting all three (cheap storage, cheap compute, cheap operations) is basically an engineering utopia.&lt;/p&gt;

&lt;p&gt;So we chose a simpler operational centre of gravity: store raw, schema-flexible data in MongoDB in a way that remains queryable day one, and treat plan correlation and corrections as additional metadata and derived layers rather than rewrites of the original values. It’s not the canonical data lake implementation, but it matched the shape of the problem we actually had: uncertainty first, meaning later, and a constant need to inspect and evolve the story without losing the original facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Databases Walk Into a Bar
&lt;/h2&gt;

&lt;p&gt;Data lakes, despite their reputation for being “just storage”, almost never exist without some notion of a catalogue. Once raw data starts accumulating, you very quickly need a way to answer basic questions: what datasets exist, what they represent, where they live, and how they are meant to be used. Without that layer, a lake stops being flexible and starts being opaque.&lt;/p&gt;

&lt;p&gt;In most standard data lake architectures, this catalogue emerges as a separate system. Technologies like Hive Metastore, AWS Glue, Unity Catalog, or similar services exist to map logical datasets to physical storage, track schemas, and help query engines make sense of otherwise passive files. The catalogue doesn’t replace the lake; it makes it navigable.&lt;/p&gt;

&lt;p&gt;PostgreSQL is not what usually comes to mind when people talk about data lake catalogues. But at its core, a catalogue is simply structured metadata: names, identifiers, relationships, and lifecycle information that need to be queryable, consistent, and understandable by both humans and systems.&lt;/p&gt;

&lt;p&gt;Seen through that lens, PostgreSQL works exceptionally well. It gives us strong consistency, a rich query model, transactional updates, and a familiar interface for expressing relationships and constraints. Instead of discovering metadata by scanning storage or inferring schemas after the fact, we explicitly record what exists and how it should be interpreted. The result is not a less capable catalogue, but a more intentional one that is built around accessibility and correctness rather than engine integration.&lt;/p&gt;

&lt;p&gt;PostgreSQL became the place where we indexed meaning: which datasets exist, what context applies, and how raw collections should be interpreted at a given point in time. &lt;/p&gt;

&lt;p&gt;And that, more or less, is how two databases walked into a bar and agreed to share custody of the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Data Lake Is a Zoo, Mine Just Has Signs
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Yes, I could discover everything by parsing collection names.&lt;br&gt;
I could also parse raw bytes in hex.&lt;br&gt;
I choose civilisation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A data lake without a catalogue is not empty, it’s just loud. All the information is technically there, but understanding it requires effort, context, and a tolerance for archaeology. Storage can be self-describing in theory, yet still deeply unfriendly in practice.&lt;/p&gt;

&lt;p&gt;Our raw data is, by design, physically self-describing. Collection names encode the same information you would normally express through object-storage paths. For example, a MongoDB collection like:&lt;br&gt;
&lt;code&gt;raw_123_foo_2026-01&lt;/code&gt;&lt;br&gt;
carries the same meaning as a more traditional data lake layout using partitioned object storage, such as: &lt;code&gt;~/qualifier=123/dataset=foo/date=2026-01/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In both cases, the dataset, scope, and time window are embedded directly into the storage structure. With enough convention and enough discipline, the data can always be rediscovered by inspecting storage alone.&lt;/p&gt;

&lt;p&gt;The problem is not whether this works, but where that logic lives. Without a catalogue, every consumer needs to know how to parse collection names, reconstruct time ranges, and apply the same concatenation rules consistently. That logic inevitably leaks into multiple services, scripts, and mental models.&lt;/p&gt;

&lt;p&gt;Instead, we centralise that knowledge in the catalogue. Rather than forcing every consumer to understand naming conventions or storage layouts, we record the relationships explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| plan | dataset_type | from       | to         | collection         |
| 123  | raw          | 2026-01-01 | 2026-01-31 | raw_123_foo_2026-01 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers no longer need to care whether the underlying data lives in MongoDB collections or in partitioned object storage (for example AWS S3 or Azure Data Lake Storage). They simply ask the catalogue what data exists for a given plan and time range, and receive the location that matches. Storage remains self-describing; the catalogue just makes that description immediately accessible.&lt;/p&gt;

&lt;p&gt;This does not replace self-description with abstraction. If the catalogue disappears, the data is still there and still interpretable by inspecting collection names directly. The system degrades into inconvenience, not failure. What the catalogue removes is the need to repeatedly rediscover the same meaning through convention and duplication.&lt;/p&gt;

&lt;p&gt;The same principle applies inside the data. Raw values are immutable, but their meaning over time is not. When a value needs to be corrected, we do not overwrite it. Instead, we attach context and let the data describe its own history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "timestamp": "2026-01-01T00:00:00Z",
  "name": "my_value",
  "value": "12",
  "metadata": {
    "status": "REPLACED",
    "collected_at": "2026-01-20T00:00:00Z",
    "...":  {... a set of audit information}
  }
},
{
  "timestamp": "2026-01-01T00:00:00Z",
  "name": "my_value",
  "value": "2",
  "metadata": {
    "status": "CURRENT",
    "collected_at": "2026-01-22T00:00:00Z",
    "...": {... a set of audit information}
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original value still exists, unchanged. What changes is not the data itself, but the context around it: a status that marks when it was superseded, an audit trail that records when and why that happened, and a new entry that assumes the role of the current value. Nothing is erased, nothing is rewritten, and every recalculation can always be traced back to the original facts.&lt;/p&gt;

&lt;p&gt;This is immutability with signage. Instead of forcing consumers to infer intent from timestamps or absence, the data makes its own history explicit. It tells you which value was current at any given time, which one replaced it, and under what circumstances - with meaning attached directly to the records themselves.&lt;/p&gt;

&lt;p&gt;Every data lake eventually becomes a zoo: full of valuable, unfamiliar creatures. Some rely on visitors memorising the animals. Mine just puts the names on the enclosures.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My data lake is self-describing.&lt;br&gt;
PostgreSQL just adds subtitles so humans can watch it without suffering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  This Was Designed Under Real-World Constraints
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;No data engineers were harmed in the making of this architecture.&lt;br&gt;
Mostly because there were none present to stop me&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This system was not designed by a specialised data platform team or under ideal conditions. It was built under a very concrete set of constraints that shaped most of the architectural decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are a small team, responsible for both building and maintaining the system, so any solution had to remain understandable and maintainable without requiring deep domain expertise in data platforms.&lt;/li&gt;
&lt;li&gt;There is very limited dedicated data management knowledge within the team, which made highly specialised data platforms and analytics stacks, lakehouse ecosystems, and custom metadata or governance platforms - unrealistic from both a development and operational perspective.&lt;/li&gt;
&lt;li&gt;The system had to be fully cloud-based, which meant that a baseline level of cost was unavoidable regardless of technology choices, making the focus more about cost predictability than absolute cost minimisation.&lt;/li&gt;
&lt;li&gt;Debuggability was treated as a first-class architectural requirement. When a calculation produces an unexpected result, the system must allow us to inspect the exact raw inputs that contributed to it immediately, not after a batch job finishes or a pipeline is re-run.&lt;/li&gt;
&lt;li&gt;The primary goal was therefore not to implement a “perfect” data lake architecture, but to build something that could be operated reliably, debugged easily, and evolved incrementally as the system and the team mature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many established data lake patterns make sense in organisations optimising for large-scale analytics and long-running batch workloads. Our constraints were different: operational clarity mattered more than theoretical optimality.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is Not Free, I Just Know What I am Paying For
&lt;/h2&gt;

&lt;p&gt;The primary trade-off in this design is storage cost. Storing raw, immutable data in MongoDB is more expensive per gigabyte than using object storage, and that cost is not always trivial to estimate upfront. The volume, shape, and retention of data vary significantly depending on the plans applied to it, and those plans can evolve or appear over time. As a result, the exact storage footprint cannot be predicted with complete confidence at the outset.&lt;/p&gt;

&lt;p&gt;However, this uncertainty is still easier to reason about than the alternative. Storage growth is largely linear and visible: data arrives, it is stored, and its cost accumulates in a predictable way over time. There are no hidden bursts of compute, no surprise cluster spin-ups, and no indirect costs tied to how often questions are asked. While the total cost may be higher, it is simpler to analyse, easier to attribute, and more transparent to operate than a model where storage is cheap but every interaction with the data incurs additional, variable processing cost.&lt;/p&gt;

&lt;p&gt;The comparison below reflects a conscious trade-off: higher storage costs, offset by more predictable spending and simpler day-to-day operations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;MongoDB-based Lake&lt;/th&gt;
&lt;th&gt;Object Storage + Lake Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage cost&lt;/td&gt;
&lt;td&gt;Higher per GB&lt;/td&gt;
&lt;td&gt;Low per GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute model&lt;/td&gt;
&lt;td&gt;Always-on database&lt;/td&gt;
&lt;td&gt;On-demand distributed compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query latency&lt;/td&gt;
&lt;td&gt;Low (interactive)&lt;/td&gt;
&lt;td&gt;Medium to high (job/cluster spin-up)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query pattern&lt;/td&gt;
&lt;td&gt;Point lookups, filtered queries&lt;/td&gt;
&lt;td&gt;Large scans, batch analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update model&lt;/td&gt;
&lt;td&gt;Metadata/status updates, new records&lt;/td&gt;
&lt;td&gt;Rewrite / compaction jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data mutability&lt;/td&gt;
&lt;td&gt;Immutable raw values, contextual updates&lt;/td&gt;
&lt;td&gt;Append-only, rewrite-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reprocessing&lt;/td&gt;
&lt;td&gt;Selective, record-level logic&lt;/td&gt;
&lt;td&gt;Batch pipeline re-execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata management&lt;/td&gt;
&lt;td&gt;Explicit catalogue (Postgres)&lt;/td&gt;
&lt;td&gt;External metastore (Glue/Hive/Unity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Explicit, application-level&lt;/td&gt;
&lt;td&gt;Platform-assisted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational complexity&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;High (multiple systems)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost predictability&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Variable (compute-driven)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling pattern&lt;/td&gt;
&lt;td&gt;Vertical + sharding&lt;/td&gt;
&lt;td&gt;Horizontal compute clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging&lt;/td&gt;
&lt;td&gt;Direct data access&lt;/td&gt;
&lt;td&gt;Indirect via jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traceability&lt;/td&gt;
&lt;td&gt;Record-level, immediate&lt;/td&gt;
&lt;td&gt;Pipeline- and job-level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to investigate issues&lt;/td&gt;
&lt;td&gt;Minutes to hours&lt;/td&gt;
&lt;td&gt;Hours to days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical team size&lt;/td&gt;
&lt;td&gt;Small to medium&lt;/td&gt;
&lt;td&gt;Medium to large&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary optimisation&lt;/td&gt;
&lt;td&gt;Operational access&lt;/td&gt;
&lt;td&gt;Analytical throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  I Didn’t Build a Perfect Data Lake, I Built One I Can Explain
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Future me will hate parts of this system.&lt;br&gt;
Present me at least knows where the data is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This architecture is not an attempt to redefine what a data lake should be, nor a manifesto against established patterns. In fact, we didn’t even come up with the idea of calling it a data lake. Using a data lake was a requirement. The name arrived fully formed, and our job was simply to make something exist behind it.&lt;/p&gt;

&lt;p&gt;With that constraint in place, we did not spend much time debating what a data lake should look like in theory. The requirement was simply that one existed. Our job was to make something that fit the description without making the system unnecessarily painful to build, operate, or explain. That’s where the cheekiness started.&lt;/p&gt;

&lt;p&gt;We kept the promises usually associated with a data lake: raw data retention, deferred interpretation, immutability, traceability - and quietly ignored the assumption that this automatically combined with large-scale distributed processing stacks and a supporting cast of coordination, orchestration, and metadata services. Not because those tools are bad, but because they optimise for a different class of problems.&lt;/p&gt;

&lt;p&gt;There is a persistent idea that data lakes are cheap. In practice, only the storage is cheap. Everything required to make that storage usable, such as distributed query engines, batch processing frameworks, orchestration layers, catalogues, and the expertise needed to operate them, carries a real and often variable cost. Object storage is inexpensive precisely because it does nothing; the moment you want to understand your data, you start paying in compute, coordination, and operational overhead.&lt;/p&gt;

&lt;p&gt;In our case, a fully cloud-based solution was also a requirement, so a baseline level of cost was unavoidable regardless of the architecture. The real choice was therefore not between cheap and expensive, but between different kinds of expense. We chose higher storage costs in exchange for simpler processes, faster feedback, and the ability to inspect and explain data directly, without spinning up clusters or waiting for pipelines to finish.&lt;/p&gt;

&lt;p&gt;So yes, this is a data lake. It stores raw data, defers meaning, preserves immutability, and supports recalculation. It just does so while being a little irreverent about the tooling, and very serious about day-to-day operability.&lt;/p&gt;

&lt;p&gt;If that makes purists uncomfortable, that’s fine.&lt;br&gt;
They weren’t in the room.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>database</category>
      <category>datascience</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Intellectual Junior Syndrome</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Sat, 24 Jan 2026 17:24:18 +0000</pubDate>
      <link>https://forem.com/srsandrade/the-intellectual-junior-syndrome-1dm3</link>
      <guid>https://forem.com/srsandrade/the-intellectual-junior-syndrome-1dm3</guid>
      <description>&lt;p&gt;The intellectual junior syndrome is a condition that primarily affects junior developers. Fresh graduates, people with a couple of years of experience, and especially the most motivated ones.&lt;/p&gt;

&lt;p&gt;It usually appears shortly after someone realises that software engineering is not just about making things work, but about doing things properly. They start reading. The whole lot: the books, the blogs, the forums.&lt;/p&gt;

&lt;p&gt;They now know what a visitor is. They know what a monad is. They know what hexagonal architecture is. They know the SOLID principles by heart. &lt;/p&gt;

&lt;p&gt;And then they get a job and a problem to solve. Not a university assignment. Not a kata. A real, messy, slightly boring business problem.&lt;/p&gt;

&lt;p&gt;And they solve it.&lt;br&gt;
In ten lines.&lt;br&gt;
Very smart lines.&lt;/p&gt;

&lt;p&gt;It’s not just a solution. It’s a framework. It’s generic. It’s extensible. It solves not just this problem, but fifty potential future problems.&lt;br&gt;
There is recursion. There is reflection. There is an interface to define the contract. There is a generic type parameter with a name nobody understands. And the whole thing lives inside a beautifully abstract method called something like process, handle, or execute.&lt;/p&gt;

&lt;p&gt;It’s elegant. It’s minimal. And it solves fifty problems.&lt;/p&gt;

&lt;p&gt;Forty-nine and a half of them are imaginary.&lt;br&gt;
The remaining half is the actual problem you asked them to solve.&lt;br&gt;
And only half of that behaviour is correct.&lt;/p&gt;

&lt;p&gt;The intellectual junior syndrome is a serious condition. In most cases it fades with experience, but in some cases it becomes chronic and persists well into senior years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clinical Observations
&lt;/h2&gt;

&lt;p&gt;Over the past months, I’ve observed several patients presenting clear symptoms of the intellectual junior syndrome. Names have been changed to protect the innocent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior A&lt;/strong&gt;&lt;br&gt;
Junior A is confronted with a simple conditional flow: a switch statement with three cases.&lt;br&gt;
Their immediate thought: replace it with a strategy pattern.&lt;/p&gt;

&lt;p&gt;Now we have three classes, one interface, a factory, and a dependency injection configuration.&lt;/p&gt;

&lt;p&gt;The original problem had three branches.&lt;br&gt;
The new solution has eight files.&lt;br&gt;
The code is now “more extensible”. Nothing has extended.&lt;/p&gt;

&lt;p&gt;Junior A never got the chance to finish the implementation. He was given medication and told to rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior B&lt;/strong&gt;&lt;br&gt;
Junior B notices that two different flows share the same two lines of code.&lt;/p&gt;

&lt;p&gt;They decide to redesign the entire module around an abstract base class.&lt;br&gt;
The abstract class is used like a utility class. Nothing is overridden. No polymorphism is needed.&lt;/p&gt;

&lt;p&gt;Two weeks later, a third flow appears.&lt;br&gt;
It doesn’t fit the hierarchy.&lt;br&gt;
Now the module needs to be redesigned again.&lt;/p&gt;

&lt;p&gt;Junior B is currently under observation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior A&lt;/strong&gt;&lt;br&gt;
Senior A proposes introducing a strategy pattern for something that could be solved with a single if.&lt;/p&gt;

&lt;p&gt;A new analysis is requested. Similar incidents have been reported before.&lt;br&gt;
Senior A was also given medication and told to rest.&lt;/p&gt;

&lt;p&gt;At this point, it becomes clear that the syndrome is not always cured by time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior A (again)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Senior A, in a Spring Boot application, presents an urgent proposal: implement a bespoke JSON parser.&lt;/p&gt;

&lt;p&gt;Clinical interview reveals no current requirement. The justification is purely prophylactic:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What if one day we need special handling?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Diagnosis is confirmed. Quarantine protocol has been applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Books Are Not the Pathogen
&lt;/h2&gt;

&lt;p&gt;It’s important to clarify one thing: the problem is not theory.&lt;br&gt;
The intellectual junior does not suffer from reading too much, but from not yet knowing what to do with what they have read.&lt;/p&gt;

&lt;p&gt;Understanding design patterns is essential. Knowing architectural styles is useful. Learning about functional programming, object orientation, or system design is valuable. None of that is wasted effort.&lt;/p&gt;

&lt;p&gt;In fact, much of what modern frameworks do is exactly this: they embed decades of design patterns so you don’t have to reimplement them every time. Frameworks like Spring Boot or Quarkus, for example, hide a huge amount of complexity behind sensible defaults and conventions.&lt;/p&gt;

&lt;p&gt;The problem starts when theory becomes a hammer and every problem starts looking like a nail.&lt;/p&gt;

&lt;p&gt;Sometimes you really do have fifty different concerns in the same piece of code. Sometimes you really do need multiple layers of abstraction. Sometimes you really do need to introduce patterns explicitly.&lt;br&gt;
But recognising those situations requires something theory alone does not provide: context, experience, and judgement.&lt;/p&gt;

&lt;p&gt;And that is precisely what the intellectual junior has not accumulated yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cure Is Simplicity
&lt;/h2&gt;

&lt;p&gt;The uncomfortable truth behind all of this is that the cure is not more knowledge. It is simplicity.&lt;/p&gt;

&lt;p&gt;And simplicity is hard. Not trivially simple code, but code that is simple because it only solves the real problem and nothing else.&lt;/p&gt;

&lt;p&gt;Creating that kind of simplicity requires knowing exactly what you need. And to know that, you need to understand the world you’re working in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maybe you won’t over-engineer something that the framework already does for you, but for that, you need to actually understand the framework.&lt;/li&gt;
&lt;li&gt;Maybe you won’t invent imaginary problems, but for that, you need to understand the business context and what problems actually exist.&lt;/li&gt;
&lt;li&gt;Maybe you won’t design complex infrastructure abstractions, but for that, you need to understand how your deployment, platforms, and tooling already behave.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simplicity is not a lack of knowledge.&lt;br&gt;
It is the result of a lot of knowledge applied with restraint.&lt;br&gt;
And for a junior, that’s genuinely hard. Not impossible, but hard. It requires mentoring, exposure, and a willingness to learn things that go far beyond the tasks they are assigned.&lt;/p&gt;

&lt;p&gt;That part is normal.&lt;/p&gt;

&lt;p&gt;The bigger concern is the seniors who never respond to the treatment. They accumulate patterns, frameworks, and rules - but not context.&lt;/p&gt;

&lt;p&gt;They stay inside a very narrow technical world and become evangelists of practices rather than observers of reality. At that point, the syndrome takes a form of its own. It no longer just affects the host - it starts trying to contaminate others.&lt;/p&gt;

&lt;p&gt;And then the final symptom appears: future-ology.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What if we need this later?”&lt;br&gt;
“What if this grows?”&lt;br&gt;
“What if requirements change?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem is that humans are terrible at predicting the future. If we were good at it, we’d probably be better off buying lottery tickets.&lt;br&gt;
In practice, when a simple case becomes complex, it is far easier to refactor a switch with three branches than to untangle a whole architecture that was built for problems that never existed.&lt;/p&gt;

&lt;p&gt;The senior who is never cured often doesn’t even get the chance to be cured.&lt;/p&gt;

&lt;p&gt;They move from project to project, design a skeleton, introduce patterns, create abstractions, and then leave to evangelise somewhere else.&lt;/p&gt;

&lt;p&gt;And the team that stays behind is left with a solution that was never really designed for their world, their constraints, or their actual problems.&lt;/p&gt;

&lt;p&gt;Just for someone else’s imagination of the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discharge Notes
&lt;/h2&gt;

&lt;p&gt;There is no shame in juniors who over-engineer. Many of us have suffered from the syndrome at some point. I was clinically diagnosed.&lt;br&gt;
And honestly, I would still much rather work with a junior who reads too much and overthinks everything than with one who couldn’t care less. The first one at least has the curiosity and the motivation. The second one has already given up before learning anything.&lt;/p&gt;

&lt;p&gt;As a former patient, I only really got a chance to recover when I was moved to projects full of dead code left behind by seniors like the ones described above. After repeatedly hitting my head trying to fit a simple if into a hundred files of unnecessary abstractions, I slowly learned something important: most of the time, I would save far more time by understanding the business and removing code than by adding more.&lt;/p&gt;

&lt;p&gt;And when removing it was not possible, I was left with a quiet, internal resentment towards the people who had already moved on.&lt;/p&gt;

&lt;p&gt;Now, as a senior - cured of this syndrome, but probably afflicted with others - I try to push a slightly different perspective.&lt;/p&gt;

&lt;p&gt;Writing code is the easiest part of our job.&lt;br&gt;
Understanding what is the minimal code we need to write is the hard part.&lt;/p&gt;

&lt;p&gt;And to get there, they don’t just need to learn languages, patterns, and frameworks. They need to understand a bit of everything: the business, the infrastructure, the constraints, the people, and the history of the system.&lt;/p&gt;

&lt;p&gt;Because anyone can build something.&lt;br&gt;
Figuring out what not to build takes longer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s1ey4so53186b903fdq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s1ey4so53186b903fdq.png" alt=" " width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>No Dogma: Applying BDD to real systems</title>
      <dc:creator>Sara A.</dc:creator>
      <pubDate>Thu, 22 Jan 2026 21:47:58 +0000</pubDate>
      <link>https://forem.com/srsandrade/no-dogma-applying-bdd-to-real-systems-3i52</link>
      <guid>https://forem.com/srsandrade/no-dogma-applying-bdd-to-real-systems-3i52</guid>
      <description>&lt;h1&gt;
  
  
  Behaviour Is Not a Methodology
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;How BDD actually works once you stop treating it like one&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I first tried to implement Behaviour-Driven Development, it didn’t go particularly well.&lt;br&gt;
Not because the team resisted it, but because I treated it too literally.&lt;br&gt;
This article is about what actually made it work - especially in low-level and technical systems.&lt;/p&gt;

&lt;p&gt;I focused on Gherkin, on syntax, on “proper” scenarios, and on reproducing what the books and talks described.&lt;br&gt;
What I got was a lot of feature files, a lot of glue code, and very little shared understanding.&lt;/p&gt;

&lt;p&gt;In practice, I was spending a lot of time translating low-level concepts - things like firmware updates, communication protocols, or system states - into high-level “natural language” scenarios that didn’t really help anyone. They were too abstract to guide implementation, and at the same time not concrete enough to give confidence when discussing behaviour with the client or product owner.&lt;/p&gt;

&lt;p&gt;The second time I tried BDD, I stopped trying to implement BDD as described, and started trying to implement communication that actually worked.&lt;/p&gt;

&lt;p&gt;Since then, I’ve seen BDD become genuinely useful but only when it adapts to the reality of the project and the people involved. I’ve used classic Gherkin, modified Gherkin, Excel sheets with formulas as behavioural models, diagrams, and UI mocks.&lt;/p&gt;

&lt;p&gt;All of them worked. None of them were “pure”.&lt;br&gt;
And that’s the core lesson behind everything in this article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BDD fails as a framework and succeeds as a forcing function for communication and only works once you stop following it and start bending it.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Behaviour Is Not a Language Problem
&lt;/h2&gt;

&lt;p&gt;Most BDD material assumes that behaviour should be expressed in “natural language”.&lt;/p&gt;

&lt;p&gt;That sounds reasonable until you remember one uncomfortable fact: natural language is subjective.&lt;/p&gt;

&lt;p&gt;Humans struggle to express what they want even in everyday life. Ask two people to agree on dinner and you’ll get friction. Yet we expect the same people to write “clear, universal, executable specifications” in English.&lt;/p&gt;

&lt;p&gt;This gets even worse when not everyone comes from the same country or culture. Even the same word can carry different meanings depending on context (British and American English are full of subtle differences). And once you add people working in a second language, the idea of a single, shared “natural” language becomes even more fragile.&lt;/p&gt;

&lt;p&gt;Natural language is ambiguous, culturally biased, emotionally loaded, and interpreted differently by different people.&lt;/p&gt;

&lt;p&gt;So the real rule is not “use natural language”.&lt;/p&gt;

&lt;p&gt;The real rule is: Use whatever medium creates shared understanding fastest.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Sometimes that’s text.  &lt;br&gt;
Sometimes it’s tables.  &lt;br&gt;
Sometimes it’s diagrams.  &lt;br&gt;
Sometimes it’s pictures.  &lt;br&gt;
Sometimes it’s Excel with formulas.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;All of these are valid expressions of behaviour.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Myth of Universal Natural Language
&lt;/h2&gt;

&lt;p&gt;BDD literature often implies that scenarios should be understandable by everyone.That is impossible. We call it ubiquitous language.&lt;/p&gt;

&lt;p&gt;There is no universal natural language. Language is always cultural and contextual. And in our lives, business driven.&lt;/p&gt;

&lt;p&gt;“Natural” for a lawyer is not natural for a developer.  “Natural” for a control engineer is already technical. And that’s fine.&lt;/p&gt;

&lt;p&gt;If all stakeholders understand protocols, standards, schemas, or state machines, then those are the natural language of the business.&lt;/p&gt;

&lt;p&gt;A perfectly valid BDD scenario can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given the device is in state CONNECTED
When a READ request with OBIS code X is sent
Then the system must respond with frame Y within 200ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;That is technical.  &lt;br&gt;
That is business-relevant.  &lt;br&gt;
That is natural for that domain.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Simplifying it would make it less true, not more accessible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Behaviour Can Be Expressed in Many Forms
&lt;/h2&gt;

&lt;p&gt;In one project or business area, behaviour can be best expressed as Excel:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given these inputs → apply these formulas → this is the output.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Rows are scenarios.
&lt;/li&gt;
&lt;li&gt;Columns are inputs and expected results.
&lt;/li&gt;
&lt;li&gt;Clients understand it. Developers understand it. Tests can be generated from it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In another project, behaviour may be best expressed with UI mocks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Given you are here (picture), with specific options selected, configuration values set, feature flags enabled, and the system already in a particular state,&lt;br&gt;&lt;br&gt;
when you click this (picture),&lt;br&gt;&lt;br&gt;
then this appears (picture).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All of that context was captured naturally in a single visual. Trying to express the same thing purely in text would have resulted in long scenarios full of &lt;em&gt;and&lt;/em&gt;, &lt;em&gt;and&lt;/em&gt;, &lt;em&gt;and&lt;/em&gt; connectors and still less precision.&lt;/p&gt;

&lt;p&gt;BDD does not require prose.&lt;br&gt;&lt;br&gt;
It requires a shared behavioural model.&lt;/p&gt;

&lt;p&gt;Language is just one possible encoding. Ultimately you might even reach the conclusion that you need multiple types of media to best convey your system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automating BDD Without Cucumber
&lt;/h2&gt;

&lt;p&gt;BDD is not about making scenarios executable.&lt;/p&gt;

&lt;p&gt;It is about making behaviour traceable and verifiable.&lt;br&gt;
The only real automation requirements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can trace tests to agreed behaviour,
&lt;/li&gt;
&lt;li&gt;you can produce (good) reports showing scenario coverage and status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If tests are a perfect 1:1 translation of scenarios, great.&lt;br&gt;&lt;br&gt;
If not, but they still give traceability and evidence, also great.&lt;/p&gt;

&lt;p&gt;BDD automation is an evidence system, not a syntax engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Language Decays are Avoidable
&lt;/h2&gt;

&lt;p&gt;BDD assumes a shared language exists. Reality: shared language drifts over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People join.
&lt;/li&gt;
&lt;li&gt;People leave.
&lt;/li&gt;
&lt;li&gt;Terms get overloaded.
&lt;/li&gt;
&lt;li&gt;Meanings become implicit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six months later, the same word means different things to different people and nobody notices. So BDD requires something most teams never build: a maintained glossary.&lt;/p&gt;

&lt;p&gt;Not documentation for its own sake, but a semantic source of truth. Every time someone asks “what does this mean?”, there must be a place to consult. And if that place does not exist, that’s not a discussion - that’s a missing artefact.&lt;/p&gt;

&lt;p&gt;Language is infrastructure. If you don’t maintain it, behaviour becomes ambiguous again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Development and Testing Cannot Be Isolated
&lt;/h2&gt;

&lt;p&gt;BDD assumes that behaviour is a shared concern: that design decisions, implementation, and validation all revolve around the same understanding of how the system should behave.&lt;/p&gt;

&lt;p&gt;Most organisations, however, are structured around strong role separation: design happens in one place, implementation in another, and validation in yet another, often across different teams and phases.&lt;/p&gt;

&lt;p&gt;These two ideas are fundamentally incompatible.&lt;/p&gt;

&lt;p&gt;The emulator problem is a perfect illustration of this.&lt;/p&gt;

&lt;p&gt;In complex systems, especially those that integrate with external platforms or hardware, meaningful testing often requires emulators or other types of test doubles. Testers depend on them to validate behaviour, but only developers usually have the technical knowledge to build them. Product owners don’t own them, budget rarely plans for them, and testers  cannot realistically implement them.&lt;/p&gt;

&lt;p&gt;So emulators become a kind of no-man’s land: critical for system validation, but owned by no one. They are built late, are often incomplete, and quickly become fragile and outdated.&lt;/p&gt;

&lt;p&gt;At that point, system behaviour is something that is discovered afterwards. You cannot have shared ownership of behaviour and at the same time isolate responsibility for validation. The moment behaviour is validated only after “development is done”, it stops driving design and becomes post-mortem verification.&lt;/p&gt;

&lt;p&gt;Which defeats the purpose.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Knowledge Gap Problem
&lt;/h2&gt;

&lt;p&gt;There is another pattern that often appears in organisations with strong role separation: dedicated validation teams frequently have little to no deep technical knowledge of the systems they are validating. In some cases, these teams are not technical at all, by design.&lt;/p&gt;

&lt;p&gt;This is often justified using the classic test pyramid: developers are responsible for unit and integration tests, while testers/validators focuses on system and acceptance testing at the top.&lt;br&gt;
The pyramid itself is not the problem. Having multiple layers of testing is generally a good idea. The problem arises when the pyramid is used to justify a separation of people, rather than a separation of concerns.&lt;/p&gt;

&lt;p&gt;On paper, this model looks clean. In practice, it creates a structural knowledge gap.&lt;/p&gt;

&lt;p&gt;Validation teams are expected to validate end-to-end behaviour, but are rarely given the technical context required to do so. They often cannot realistically automate tests themselves. At best, they can adjust pre-made scripts or operate tools prepared by others, but they are structurally unable to design test infrastructure, build emulators, or reason about system-level behaviour.&lt;br&gt;
This can work reasonably well for user-facing applications, where behaviour is mostly observable through interfaces and workflows. But it breaks down completely in more technical domains.&lt;/p&gt;

&lt;p&gt;If you are testing an embedded system, for example, you cannot meaningfully validate behaviour without understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the underlying protocols and communication patterns,
&lt;/li&gt;
&lt;li&gt;the difference between technologies (e.g. DLMS, Zigbee, or similar standards),
&lt;/li&gt;
&lt;li&gt;the configuration and state of the device,
&lt;/li&gt;
&lt;li&gt;the constraints and failure modes of the hardware itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these contexts, black-box testing is often not just insufficient - it is actively misleading. The most important behaviours are not visible at the UI level and cannot be reasoned about without technical context.&lt;/p&gt;

&lt;p&gt;Which again contradicts the idea that behaviour can be validated without understanding the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Behaviour Is Necessary, Not Sufficient
&lt;/h2&gt;

&lt;p&gt;Scenarios define what the system should do.&lt;br&gt;
They rarely define performance, resilience, security, or regulatory constraints.&lt;br&gt;
Yet those are often the real reasons systems fail.&lt;/p&gt;

&lt;p&gt;For example, contracts sometimes explicitly or implicitly require that the system needs to comply with ISO 27001 or some other framework.&lt;/p&gt;

&lt;p&gt;Very rarely do behavioural scenarios capture what that actually means in practice: audit trails, access control policies, incident response procedures, data retention rules, and similar constraints.&lt;/p&gt;

&lt;p&gt;This is a structural blind spot in many BDD examples: non-functional and regulatory aspects are often omitted, even though they are frequently the most critical requirements in real systems.&lt;/p&gt;

&lt;p&gt;In practice, scenarios alone are not enough to drive engineering decisions.&lt;/p&gt;

&lt;p&gt;Ultimately, these aspects can also be modelled using scenarios, but doing so often becomes an exercise in translating existing requirements into a different format, rather than improving understanding.&lt;br&gt;
Architecture notes, protocol references, regulatory standards, and technical constraints must live alongside behavioural scenarios.&lt;/p&gt;

&lt;p&gt;Not in a separate universe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Behaviour Beyond the System
&lt;/h2&gt;

&lt;p&gt;One last point that is often overlooked: behaviour should not only condition development, but everything around it.&lt;/p&gt;

&lt;p&gt;If BDD is really about shared understanding of behaviour, then that understanding should apply not just to the system, but also to the way people work together.&lt;/p&gt;

&lt;p&gt;That includes the team. But it also includes the client/product-owner.&lt;/p&gt;

&lt;p&gt;We all know about scope creep. We all know about fixed-price projects that claim to be agile in scope, but not in budget. A lot of conflict in software projects does not come from technical failure, but from mismatched expectations about how collaboration itself should work.&lt;/p&gt;

&lt;p&gt;In fixed-price or highly constrained projects, one way to address this is to explicitly define behavioural expectations as part of the contract or engagement model.&lt;/p&gt;

&lt;p&gt;Not in abstract terms, but very concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scenarios are defined collaboratively in workshops,&lt;/li&gt;
&lt;li&gt;client input is required within specific timeframes,&lt;/li&gt;
&lt;li&gt;test evidence and traceability are provided as part of each delivery by delivering X and Y reports,&lt;/li&gt;
&lt;li&gt;demos happen at an agreed frequency and in a specific format,&lt;/li&gt;
&lt;li&gt;and anything outside of that flow is explicitly considered out of scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Does the client require a specific documentation template? If so, it should be captured in this document. And if the client does not require any specific template, that should be captured as well.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Will the client do any testing themselves? If yes, write down which level is their responsibility. And write down which levels of testing are covered by your team.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The same idea can apply internally within a team: documenting code practices, review standards, how demos are conducted, how decisions are made, and what “done” actually means.&lt;/p&gt;

&lt;p&gt;Not as bureaucracy, but as shared behavioural expectations.&lt;/p&gt;

&lt;p&gt;Because in the end, most project failures are not caused by missing features. They are caused by people having different mental models of what “collaboration” was supposed to look like.&lt;/p&gt;

&lt;p&gt;Making that behaviour explicit is, in many cases, more important than any technical scenario.&lt;br&gt;
This is just one possible format and other approaches can work just as well, but the underlying principle is the same: behaviour should be explicit, shared, and continuously validated.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to keep from all this rambling
&lt;/h2&gt;

&lt;p&gt;If there is anything worth keeping from all of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Behaviour first - but there are many valid ways to express it.
&lt;/li&gt;
&lt;li&gt;Natural language - only within a specific context.
&lt;/li&gt;
&lt;li&gt;Tests - traceable to behaviour. Tests, tests, tests.
&lt;/li&gt;
&lt;li&gt;Tests as living documentation - the best ramp-up material.
&lt;/li&gt;
&lt;li&gt;No strict phase separation - a feature is only done when behaviour works and is validated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If, after reading all this, you realise that your team already does most of it, just without calling it BDD, then congratulations.&lt;/p&gt;

&lt;p&gt;You were never missing a framework. You were already doing Behaviour-Driven Development.&lt;/p&gt;

&lt;p&gt;And if someone tells you that what you are doing is “not really BDD”, that’s fine too. Labels matter much less than outcomes.&lt;/p&gt;

&lt;p&gt;And if this approach doesn’t work in your context, that’s also fine. The point is not to copy a process, but to find your own way of following the same principles.&lt;/p&gt;

&lt;p&gt;That, in the end, is what BDD was always meant to be about.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>bdd</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
