<?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>DEV Community: Kamen</title>
    <description>The latest articles on DEV Community by Kamen (@kamenivanov).</description>
    <link>https://dev.to/kamenivanov</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4043234%2Fc0c83e8a-752a-435b-b342-4a44ed85593a.png</url>
      <title>DEV Community: Kamen</title>
      <link>https://dev.to/kamenivanov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kamenivanov"/>
    <language>en</language>
    <item>
      <title>Testing the Service Layer - Part 2: Where the Shared Ancestor Ends (Chapter 10)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 21 Sep 2026 08:48:39 +0000</pubDate>
      <link>https://dev.to/kamenivanov/testing-the-service-layer-part-2-where-the-shared-ancestor-ends-chapter-10-1m17</link>
      <guid>https://dev.to/kamenivanov/testing-the-service-layer-part-2-where-the-shared-ancestor-ends-chapter-10-1m17</guid>
      <description>&lt;p&gt;Part 1 of this chapter was about everything &lt;code&gt;AbstractCrudServiceTestCase&lt;/code&gt; gets to say once, for every CRUD service that will ever extend it - ownership stamping, idempotent delete, the authorization path shared by &lt;code&gt;update()&lt;/code&gt;, &lt;code&gt;loadById()&lt;/code&gt;, and &lt;code&gt;delete()&lt;/code&gt; alike. That suite works precisely because the behavior it covers really is shared: the same rule, expressed once, correctly inherited by anything that extends the base class. But not everything on &lt;code&gt;ProductsServiceImpl&lt;/code&gt; and &lt;code&gt;CategoriesServiceImpl&lt;/code&gt; fits that description, and the clearest example is a method the abstract suite never touches at all.&lt;/p&gt;




&lt;h3&gt;
  
  
  Twins that aren't ancestors
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;changeStatus()&lt;/code&gt; exists independently on both concrete services, and today, the two implementations are nearly identical:&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;@Transactional&lt;/span&gt;
&lt;span class="nd"&gt;@Override&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;changeStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;requesterId&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;requesterId&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthorizationException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UNAUTHORIZED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loadOrThrowNotFound&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;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;authorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&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;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;newStatus&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="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUpdatedById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;A same-status request deserved its own explicit decision rather than being left to chance. &lt;code&gt;ProductStatus.ACTIVE.canTransitionTo(ACTIVE)&lt;/code&gt; returns false, so without the guard above, requesting a transition to the status a product already holds would throw &lt;code&gt;IllegalStateException&lt;/code&gt;. A client retrying a timed-out request would see a failure for a change that had already succeeded. The check mirrors &lt;code&gt;delete()&lt;/code&gt;'s idempotency rather than inventing a new definition of it. If nothing would actually change, nothing gets written, not even the audit fields. A retry that touched &lt;code&gt;updatedById&lt;/code&gt; on a no-op would leave the audit trail implying movement that never happened. The authorization check still runs before the status comparison. A non-owner requesting the current status doesn't get a silent pass just because nothing would change. &lt;/p&gt;

&lt;p&gt;It would be tempting to read that duplication as an oversight. Surely this belongs on &lt;code&gt;AbstractCrudService&lt;/code&gt;, right next to &lt;code&gt;create()&lt;/code&gt; and &lt;code&gt;update()&lt;/code&gt;. It doesn't, for two separate reasons, and both of them are more instructive than the duplication itself.&lt;/p&gt;

&lt;p&gt;The first is structural: not every domain in this system has a status at all. &lt;code&gt;AbstractCrudService&lt;/code&gt; is generic over any &lt;code&gt;Domain extends AbstractAuditable&amp;lt;UUID&amp;gt;&lt;/code&gt;, and forcing a status concept onto that hierarchy would mean either a generic hook that adds ceremony to every future service that never needs status, for example an &lt;code&gt;Order&lt;/code&gt; that has no status field to speak of, or a leaky assumption baked into a base class that should know nothing about what any particular domain represents. &lt;code&gt;AbstractAuditable&lt;/code&gt; knows about identity and ownership, because every domain object in this system has those. It doesn't know about status, because most won't.&lt;/p&gt;

&lt;p&gt;The second reason is the one that actually matters going forward, and it's about where these two methods are headed, not where they are today. &lt;code&gt;Product&lt;/code&gt; and &lt;code&gt;Category&lt;/code&gt; don't share transition rules: &lt;code&gt;ProductStatus&lt;/code&gt; allows a product to move from &lt;code&gt;ACTIVE&lt;/code&gt; to &lt;code&gt;OUT_OF_STOCK&lt;/code&gt; and back, but &lt;code&gt;CategoryStatus&lt;/code&gt; has no equivalent at all. And the side effects each &lt;code&gt;changeStatus()&lt;/code&gt; will eventually need to trigger are going to diverge in a way that has nothing to do with today's identical-looking code. When this system integrates with Kafka for event broadcasting, publishing a product to &lt;code&gt;ACTIVE&lt;/code&gt; will very likely need to fire an event - a storefront needs to know a product became purchasable. Archiving a category may need to fire an event too, but only on that specific transition, not on every status change a category goes through. Those are two different rules about two different triggers, and they belong on two different methods, sitting inside two different classes, exactly where they already are.&lt;/p&gt;

&lt;p&gt;If that behavior had been forced into a shared base method ahead of time - a generic &lt;code&gt;onStatusChanged()&lt;/code&gt; hook, parameterized somehow to cover both cases, the divergence would have had nowhere clean to go. You'd end up either parameterizing the hook into meaninglessness, threading flags and conditionals through a method that was supposed to be simple, or growing base-class conditionals that only apply to some subclasses, which is the exact kind of rot this whole series argues against. Two classes having identical code today is evidence of nothing on its own. The real question is whether they're identical because they're expressing the same concept, or identical because they haven't yet diverged. &lt;code&gt;authorize()&lt;/code&gt; is the same rule in both classes and will very likely stay the same rule, because ownership-based authorization isn't domain-specific - it's genuinely shared ancestry. &lt;code&gt;changeStatus()&lt;/code&gt; is the same code in both classes today for the far less interesting reason that neither one has grown its side effects yet. Promoting the first into the base class was correct. Promoting the second would have been premature abstraction wearing the same clothes as legitimate reuse and premature abstraction is arguably the worse failure of the two, because duplication is at least honest about not knowing yet, while a wrong abstraction actively resists the divergence when it finally shows up.&lt;/p&gt;

&lt;p&gt;The test suite reflects that boundary. Each concrete class carries its own full set of &lt;code&gt;changeStatus()&lt;/code&gt; tests - null requester, missing entity, wrong owner, valid transition, illegal transition and none of that coverage comes from the abstract suite, because none of it can:&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;changeStatus_WhenTransitionIsIllegal_ShouldThrowAndNeverPersist&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persistedProduct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ARCHIVED&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;productDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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;IllegalStateException&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;productsService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;changeStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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="nc"&gt;ProductStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACTIVE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&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;productDao&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;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&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;That's not a gap in the abstraction. It's the cost of the abstraction being drawn in the right place. "Extend, don't rewrite" only works as a principle if you're also willing to say, clearly, which things were never meant to be extended from a shared ancestor in the first place. Worth naming directly, while we're on &lt;code&gt;changeStatus()&lt;/code&gt;: nothing in this suite would have caught it missing &lt;code&gt;@Transactional&lt;/code&gt;. Both implementations follow the same read-modify-write shape as &lt;code&gt;update()&lt;/code&gt; - load, mutate, persist and &lt;code&gt;update()&lt;/code&gt; carries that annotation for exactly the reason discussed in Chapter 9: it's declarative metadata, not a framework leak, and it matters more once a side effect like Kafka event publishing eventually sits inside the same method. A mocked-DAO test verifies what a method does which calls happen, in what order, under what conditions, but the transactional boundary around those calls is applied by a Spring AOP proxy that never exists in this test setup at all. Catching a missing or misplaced &lt;code&gt;@Transactional&lt;/code&gt; requires an integration test with a real Spring context, which is precisely the layer this chapter chose not to test. That's not a flaw in the mocked-DAO approach - it's a reminder that "100% service-layer coverage" from unit tests alone was never actually 100% of what could go wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  A specification hiding in a branch
&lt;/h2&gt;

&lt;p&gt;Coverage tooling can tell you that a line or branch executed. It cannot tell you whether the test data and assertions proved the behavior that branch exists to protect.&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;()&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="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSpecification&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;ProductSpecification&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;getDimensions&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;getWeight&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;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setDimensions&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;getDimensions&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setWeight&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;getWeight&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;Before this test was added, every update test built an existing product without a specification. The update path therefore always took the &lt;code&gt;if&lt;/code&gt; branch and created a new &lt;code&gt;ProductSpecification&lt;/code&gt;. The &lt;code&gt;else&lt;/code&gt; branch - the normal path for a product that already has a specification was never exercised with a fixture that could prove its intended behavior: mutating the existing object in place rather than replacing it with an equal-looking new one.&lt;/p&gt;

&lt;p&gt;The gap was easy to miss because the suite still had broad coverage around updates, and the transformer appeared to work for the creation case. But a flipped condition, a replacement where mutation was intended, or a broken in-place update could all survive until production. The missing test was not merely about increasing a coverage percentage, It had to make the identity of the existing &lt;code&gt;ProductSpecification&lt;/code&gt; part of the contract. The fix needed the domain to carry meaningful dimensions and weight, plus a test that could distinguish “the transformer mutated the existing object” from “the transformer built an equal-looking new one.” &lt;code&gt;assertEquals&lt;/code&gt; cannot make that distinction on its own:&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;update_WhenSpecificationAlreadyExists_ShouldMutateInPlaceRatherThanReplace&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persistedProduct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DRAFT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSpecification&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;ProductSpecification&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"10x10x10cm"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;450&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ProductSpecification&lt;/span&gt; &lt;span class="n"&gt;originalSpecification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;UpdateProduct&lt;/span&gt; &lt;span class="n"&gt;updateDto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createUpdateDomain&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;productDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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;productDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Product&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;productsService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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="n"&gt;updateDto&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The transformer's else-branch mutates the existing specification rather than&lt;/span&gt;
    &lt;span class="c1"&gt;// allocating a new one - this asserts that behavior directly, not just its effect.&lt;/span&gt;
    &lt;span class="n"&gt;assertSame&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;originalSpecification&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="na"&gt;getSpecification&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="n"&gt;updateDto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDimensions&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="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getDimensions&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="n"&gt;updateDto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWeight&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="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getWeight&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;code&gt;assertSame&lt;/code&gt; is doing real work in that test, not just being pedantic. It's the only way to actually prove the &lt;code&gt;else&lt;/code&gt; branch ran and mutated the existing object, rather than the &lt;code&gt;if&lt;/code&gt; branch running and happening to produce a new object that merely looks equal. Those are two different code paths, guarding against two different bugs, and a weaker assertion would have let either one hide behind the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  When a fixture stops earning its keep
&lt;/h2&gt;

&lt;p&gt;The suite went through one more revision worth describing, not because the bug it fixed was dramatic, but because it happened inside the test code itself, and the same discipline that applies to production code turned out to apply there too. The original fixture for building an already-persisted entity - needed by every update, delete, and loadById test that requires an existing owner routed through the service's own &lt;code&gt;create()&lt;/code&gt; method:&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="nf"&gt;createPersistedEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;entityId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="o"&gt;())).&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;withId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getArgument&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="n"&gt;entityId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;persisted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getService&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createValidNewDomain&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ownerId&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;persisted&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="n"&gt;entityId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persisted&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="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persisted&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCreatedById&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="n"&gt;clearInvocations&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getMockDao&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;persisted&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;It worked, but it created a dependency that had no business existing: every update, delete, and loadById test in the suite now implicitly depended on &lt;code&gt;create()&lt;/code&gt; behaving correctly, purely to get a fixture into existence. If &lt;code&gt;create()&lt;/code&gt; ever broke - a transformer throwing, an authorization check misfiring, every ownership test across every concrete service would fail alongside it, for a reason that had nothing to do with what those tests were actually named after. That's a diagnostic cost paid every time someone opens a build with ten failing tests and has to work backward to discover that only one of them represents a real bug. Worse, it needed &lt;code&gt;clearInvocations()&lt;/code&gt; afterward, purely to erase the trace that fixture setup had left on the mock, a workaround whose only purpose was to hide the coupling it had just introduced. The replacement drops the indirection entirely, each concrete class already had, separately, a direct helper that builds a persisted entity through its own constructor - no mock, no service call, no cleanup required:&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="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="nf"&gt;buildPersistedEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="nf"&gt;persistedEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;ownerId&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="nf"&gt;buildPersistedEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ownerId&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nf"&gt;buildPersistedEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;ownerId&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="nf"&gt;persistedProduct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DRAFT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nf"&gt;persistedProduct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&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;Product&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Existing Product"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSku&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EXISTING-SKU"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TEN&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCreatedById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUpdatedById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;product&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;Now &lt;code&gt;withId()&lt;/code&gt; has exactly one caller left - the create-ownership test, the one place that genuinely needs to simulate what a real DAO does on persistence and &lt;code&gt;buildPersistedEntity()&lt;/code&gt; has exactly one job, building a fixture, with zero coupling to whether &lt;code&gt;create()&lt;/code&gt; happens to work on a given day. Two mechanisms had grown up to solve what was really one problem, and the fix wasn't clever, just a refusal to let that stand. It's worth naming directly: the same principle this whole chapter argues for in production code - extend or override, don't quietly duplicate a slightly different version of the same mechanism applies just as much to the tests themselves. A test suite that hasn't had this kind of pass run over it tends to accumulate exactly this shape of redundancy, because nobody sets out to write two fixture mechanisms, they arrive one at a time, each locally reasonable, and only look wrong once they're standing next to each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this buys, concretely
&lt;/h2&gt;

&lt;p&gt;Put the whole suite next to the two concrete classes and the claim this chapter opened with is checkable rather than rhetorical. &lt;code&gt;AbstractCrudServiceTestCase&lt;/code&gt; carries every test for &lt;code&gt;create()&lt;/code&gt;, &lt;code&gt;update()&lt;/code&gt;, &lt;code&gt;loadById()&lt;/code&gt;, and &lt;code&gt;delete()&lt;/code&gt; - authorization guards, not-found guards, ownership stamping, idempotent delete - written once, against the shape every future CRUD service will share. &lt;code&gt;ProductsServiceTestCase&lt;/code&gt; and &lt;code&gt;CategoriesServiceTestCase&lt;/code&gt; add only what's actually theirs: field mapping through their own transformer, the specification branch in &lt;code&gt;Product&lt;/code&gt;'s case, and a full &lt;code&gt;changeStatus()&lt;/code&gt; suite each, because that behavior was never ancestral to begin with. A future &lt;code&gt;OrdersServiceImpl&lt;/code&gt; extending &lt;code&gt;AbstractCrudService&lt;/code&gt; inherits the first category for free, the same way &lt;code&gt;ProductsServiceImpl&lt;/code&gt; and &lt;code&gt;CategoriesServiceImpl&lt;/code&gt; did, and only has to write tests for whatever is genuinely its own.&lt;/p&gt;

&lt;p&gt;That's the return on the investment this chapter spent both of its halves on: not a percentage on a coverage report, but a shrinking marginal cost for every service this codebase adds from here forward, a boundary drawn deliberately between what gets inherited and what gets grown independently, and a test suite that fails for the reason its name says it will fail, and nothing else.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;The service layer this chapter tested stays deliberately ignorant of anything outside it - no Spring context, no HTTP, no idea that a REST controller exists. That ignorance has a cost that hasn't been paid yet: when &lt;code&gt;AuthorizationException&lt;/code&gt; or &lt;code&gt;NotFoundException&lt;/code&gt; crosses out of the business logic module, something still has to decide it becomes a 401 or a 404. Chapter 11 is about where that decision lives, and why the domain layer should never be the one making it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 10, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-10-bl-testing&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-10-bl-testing" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/testing-the-service-layer-part-1-what-the-generic-suite-owes-every-service-chapter-10-2ok4"&gt;Read Chapter 10, Part 1: What the Generic Suite Owes Every Service&lt;/a&gt;&lt;br&gt;
▶️ Read Chapter 11: Error Handling and Domain Exceptions Across Module Boundaries (Coming soon)&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;Disclosure: I earn a commission if you purchase through the links below, at no additional cost to you.&lt;/p&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code&lt;/em&gt; &lt;strong&gt;&lt;em&gt;friends20&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;for an exclusive&lt;/em&gt; &lt;strong&gt;&lt;em&gt;20% discount&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>testing</category>
      <category>qa</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Testing the Service Layer - Part 1: What the Generic Suite Owes Every Service (Chapter 10)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Tue, 15 Sep 2026 12:00:50 +0000</pubDate>
      <link>https://dev.to/kamenivanov/testing-the-service-layer-part-1-what-the-generic-suite-owes-every-service-chapter-10-2ok4</link>
      <guid>https://dev.to/kamenivanov/testing-the-service-layer-part-1-what-the-generic-suite-owes-every-service-chapter-10-2ok4</guid>
      <description>&lt;p&gt;In the preceding chapters, I built a clean architecture with a distinct demarcation line between the domain model, infrastructure, and web layers. The services orchestrate business logic without any awareness of Hibernate, SQL, or HTTP protocols. I apply Spring’s &lt;code&gt;@Transactional&lt;/code&gt; declaratively solely at the service level as the transaction boundary of the Use Case, while keeping the services themselves completely clean.&lt;/p&gt;

&lt;p&gt;Now I collect the biggest dividend of that discipline: &lt;strong&gt;the ability to test the entire business logic in milliseconds, with zero dependencies on the Spring context and zero real databases&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is a fixture I'd left half-finished.&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="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getMockDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="o"&gt;())).&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getArgument&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="c1"&gt;//  domain.setId(entityId);&lt;/span&gt;
    &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCreatedById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;savedEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;domain&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;That commented-out line had been sitting there since the first draft of this fixture, a placeholder from when I was sketching out what a persisted entity should look like, meant to be finished once I got back to it. I didn't get back to it. The test above it kept passing anyway, for reasons I hadn't stopped to check, and a commented-out line that nobody's chasing has a way of looking like it isn't hiding anything. It was, once I went to actually uncomment it. &lt;code&gt;Product&lt;/code&gt; extends a base class where &lt;code&gt;id&lt;/code&gt; is declared &lt;code&gt;final&lt;/code&gt;, assigned once in the constructor, never touched again for the life of the object. There is no setter to call - the line wasn't just unfinished, it was never going to compile. Which meant the fixture had never actually simulated what it claimed to: a DAO assigning an identity on persistence. It had been silently omitting that step since the day I wrote it, and the test suite built on top of it - every update, delete, and loadById test that depends on this same fixture for a "persisted" entity had never noticed, because none of those tests were checking whether the id it produced meant anything. They were checking whether &lt;em&gt;a&lt;/em&gt; value came back, and a value always did.&lt;/p&gt;

&lt;p&gt;Once I saw it, the question stopped being "why is this line wrong" and became "why does this class refuse to let it be right." A mutable id would mean an object could change what it is mid-lifecycle - that a &lt;code&gt;Product&lt;/code&gt; persisted under one identity could quietly become a different &lt;code&gt;Product&lt;/code&gt; without anyone reassigning the reference. The equality contract further up the hierarchy is built entirely on that id. If it moved, two variables holding what used to be the same entity could silently stop agreeing on whether they still were. Making &lt;code&gt;id&lt;/code&gt; final wasn't an oversight that happened to inconvenience a test author. It was a decision that a domain object's identity is not something even its own author gets to revise after the fact and a mock that pretends otherwise isn't a shortcut, it's a small act of denying that decision ever happened. Which means the actual bug wasn't the missing setter. The actual bug was a fixture that had been left to imply persistence without ever actually simulating it. A real DAO doesn't mutate the transient instance you hand it. It takes what you give it - no identity yet, nothing persisted and it hands back something new, same data, an id that didn't exist a moment ago. The mock's job was never to make the assertion pass. It was to tell the same story a real database would tell, minus the SQL.&lt;/p&gt;

&lt;p&gt;This is the shape most of the mistakes in this chapter turned out to have. Not a typo, not a missed edge case in the everyday sense, but a test quietly rewriting the contract of the thing it was supposed to be checking, because rewriting the contract was easier than satisfying it. &lt;code&gt;AbstractCrudServiceTestCase&lt;/code&gt; - the generic suite that every CRUD service in this codebase is meant to inherit for free, three abstract hooks and nothing else to implement, had this problem baked into its shared fixture, which meant the problem wasn't confined to one test. It was inherited by every service that extended the base class, silently, the way a real trait gets inherited: without anyone deciding to pass it on. That word - inherited, is worth sitting with the rest of this chapter, because it cuts both ways, and the good half of it is the actual point. The same mechanism that let one broken fixture assumption propagate to every concrete service is the mechanism that, once fixed, lets every concrete service pick up the correct behavior without anyone touching them again. &lt;code&gt;ProductsServiceTestCase&lt;/code&gt; and &lt;code&gt;CategoriesServiceTestCase&lt;/code&gt; didn't need to relearn what ownership-stamping means, or what an idempotent delete looks like, or how authorization should fail. They inherited it - the way an organism inherits a nervous system rather than growing one from scratch each generation. And they only had to express what was actually theirs: how a &lt;code&gt;Product&lt;/code&gt; maps its own fields, what it means for a &lt;code&gt;Product&lt;/code&gt; specifically to change status, what a &lt;code&gt;Category&lt;/code&gt; does differently. Software that evolves rather than gets rewritten looks like this from the inside. Not code that never changes, but code where the right thing changes and nothing else has to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why mock the DAO instead of an in-memory database
&lt;/h2&gt;

&lt;p&gt;Before any of that inheritance can happen, there's a decision underneath it that's easy to walk past: why mock the DAO at all, instead of pointing every test at an in-memory database and calling it a day. I'd spent years testing the persistence layer that way - an H2 instance, Flyway migrations running against it, real SQL executing against a real schema and it worked, in the sense that it caught real bugs. But it was testing a different layer than the one I needed to test here, and conflating the two is exactly the kind of unexamined habit that produces slow, ambiguous test suites without anyone deciding that's what they wanted.&lt;/p&gt;

&lt;p&gt;An in-memory database test of &lt;code&gt;ProductsServiceImpl.create()&lt;/code&gt; verifies two things at once, bundled together whether you like it or not: that the service's own logic is correct, and that Hibernate's mapping, the schema, and the query it generates are all correct. When that test fails, you don't know which of those two failed without opening the stack trace and reading past the assertion into the actual exception. A &lt;code&gt;ConstraintViolationException&lt;/code&gt; on a NOT NULL column tells you the schema and the entity disagree about nullability - a real, valuable thing to know but it tells you nothing about whether &lt;code&gt;authorize()&lt;/code&gt; correctly rejects a non-owner, which is a business rule with no SQL in it at all. Persistence-layer testing, which I covered back in Chapter 7, exists precisely to isolate that first category of failure. Business-logic testing needs to isolate the second, which means the persistence layer has to be prevented from participating in the test at all. A &lt;code&gt;Dao&lt;/code&gt; mock doesn't approximate a database, it refuses to be one. &lt;code&gt;productDao.create(any())&lt;/code&gt; returns exactly what the test tells it to return, nothing more, and if &lt;code&gt;ProductsServiceImpl.create()&lt;/code&gt; is wrong, that wrongness has nowhere to hide behind an ORM quirk. The cost of that isolation is that a mocked-DAO test suite can be lying to you in a way an in-memory database test can't. Mockito doesn't know what &lt;code&gt;ProductDao.create()&lt;/code&gt; is supposed to do, it only knows what you told it to do when you wrote &lt;code&gt;when(...).thenAnswer(...)&lt;/code&gt;. If that stub embeds an assumption that doesn't match how the real &lt;code&gt;ProductDaoImpl&lt;/code&gt; actually behaves, the test can pass forever while the production code silently diverges from the fixture's model of it. That's not a hypothetical risk, it's the exact failure mode the half-finished &lt;code&gt;create()&lt;/code&gt; fixture represented, just at the interface boundary instead of inside a single test. Coverage percentage cannot see this category of bug, because coverage measures whether a line executed, not whether the value flowing through that line at test time was honest. A mocked-DAO suite earns its speed and its isolation by taking on a permanent, structural obligation: every stub has to be checked, by a human, against what the real implementation actually contracts to do - the DAO's Javadoc, its own persistence-layer tests from Chapter 7, its behavior under the constraints the database schema actually enforces. Chapter 7 and this chapter aren't separate concerns that happen to share a domain object, they're two halves of a coverage claim that's only true if both halves hold, and neither replaces the other's job.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the abstract suite actually asks of a new service
&lt;/h2&gt;

&lt;p&gt;The whole promise of the base class is that a new CRUD service - some future &lt;code&gt;OrdersServiceImpl&lt;/code&gt;, some future &lt;code&gt;WarehousesServiceImpl&lt;/code&gt; - gets four operations, fully tested, in exchange for implementing three methods: a create transformer, an update transformer, and an authorization rule. Everything else is inherited, unquestioned, from &lt;code&gt;AbstractCrudService&lt;/code&gt; and its matching &lt;code&gt;AbstractCrudServiceTestCase&lt;/code&gt;. It's worth walking through why each of those four operations is shaped the way it is, because none of the shapes were arbitrary, and a few of them only look obvious in hindsight. &lt;code&gt;create()&lt;/code&gt; has exactly one guard - a null &lt;code&gt;requesterId&lt;/code&gt; throws before anything else runs, and then it does something that's easy to read past: it stamps &lt;code&gt;createdById&lt;/code&gt; and &lt;code&gt;updatedById&lt;/code&gt; onto the domain object itself, before the DAO ever sees it. The test that proves this uses an &lt;code&gt;ArgumentCaptor&lt;/code&gt; rather than inspecting the method's return value, and the distinction matters more than it looks like it should.&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;create_WhenRequesterIdIsPresent_ShouldStampOwnershipBeforePersisting&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;NewDomain&lt;/span&gt; &lt;span class="n"&gt;newDomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createValidNewDomain&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;assignedId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="o"&gt;())).&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;withId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getArgument&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="n"&gt;assignedId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Domain&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;getService&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newDomain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;final&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;Domain&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;captor&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;times&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="na"&gt;create&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="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;domainPassedToDao&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="c1"&gt;// A transient domain has no identity until the DAO assigns one - the service must never invent an id itself.&lt;/span&gt;
    &lt;span class="n"&gt;assertNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domainPassedToDao&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="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domainPassedToDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCreatedById&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="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domainPassedToDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUpdatedById&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="n"&gt;assignedId&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="na"&gt;getId&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="n"&gt;requesterId&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="na"&gt;getCreatedById&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 test needs to distinguish two moments in time that a single returned object collapses into one: what the service handed the DAO before persistence, and what the DAO handed back after. If I only inspected &lt;code&gt;result&lt;/code&gt; - the return value of &lt;code&gt;create()&lt;/code&gt;, I'd be looking at the &lt;em&gt;post&lt;/em&gt;-persistence object, the one &lt;code&gt;withId()&lt;/code&gt; constructed, and I'd have no way of proving that &lt;code&gt;createdById&lt;/code&gt; was already set on the object &lt;em&gt;before&lt;/em&gt; it reached the DAO rather than after. That distinction sounds pedantic until you imagine the bug it protects against: a future version of &lt;code&gt;create()&lt;/code&gt; that persists the domain object first and only stamps ownership afterward, in a follow-up &lt;code&gt;dao.update()&lt;/code&gt; call. That version would produce an identical-looking &lt;code&gt;result&lt;/code&gt;. It would also mean a brief window during which an unowned entity exists in the database, which is precisely the kind of window a security review should be able to trust the test suite to have already ruled out. &lt;code&gt;ArgumentCaptor.capture()&lt;/code&gt; freezes the argument at the moment of the call, not at the moment of the test's assertion, which is the only way to make that distinction visible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;withId()&lt;/code&gt; itself exists because of the same immutability the opening story ran into. There is no &lt;code&gt;setId()&lt;/code&gt; anywhere in the hierarchy, by design, so a mock that simulates persistence has to construct a genuinely new instance carrying the assigned identity rather than mutate the one it was handed. Each concrete test class implements it once, against its own domain object's actual constructors:&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;@Override&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nf"&gt;withId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;transientProduct&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;transientProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCreatedById&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;transientProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUpdatedById&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;transientProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;transientProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSku&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;transientProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;transientProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&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;code&gt;update()&lt;/code&gt; carries a signature decision worth naming directly: &lt;code&gt;authorize(Domain domain, UUID requesterId)&lt;/code&gt; takes the full, already-loaded entity, never just its id. It would have been the more obvious signature to reach for, and it would have quietly forced every implementation to reload the entity a second time inside &lt;code&gt;authorize()&lt;/code&gt; itself, since ownership checks need to inspect &lt;code&gt;getCreatedById()&lt;/code&gt;, and an id alone tells you nothing about who created anything. Passing the already-loaded domain object means the one &lt;code&gt;dao.loadById()&lt;/code&gt; call inside &lt;code&gt;update()&lt;/code&gt;, &lt;code&gt;loadById()&lt;/code&gt;, and &lt;code&gt;delete()&lt;/code&gt; does double duty: it fetches the entity for the operation, and it hands that same instance to the authorization check without a second round trip. In a mocked-DAO test, this shows up as a single &lt;code&gt;verify(getMockDao(), times(1)).loadById(...)&lt;/code&gt; and the fact that it's &lt;code&gt;times(1)&lt;/code&gt; and not &lt;code&gt;times(2)&lt;/code&gt; is itself asserting something about the architecture, not just about the test. If a future refactor accidentally reintroduced a second load inside &lt;code&gt;authorize()&lt;/code&gt;, this test would catch it immediately, because the interaction count would change even though every return value would still look correct.&lt;/p&gt;

&lt;p&gt;The update test that checks ownership survives correctly is doing a similarly specific job, and it's worth naming exactly what regression it exists to catch, because on the surface it looks like it's testing something that can't fail:&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;update_WhenRequesterIsOwner_ShouldPersistWithUpdatedByChangedAndCreatedByPreserved&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;existingDomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persistedEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;UpdateDomain&lt;/span&gt; &lt;span class="n"&gt;updateDomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createUpdateDomain&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingDomain&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="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingDomain&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="o"&gt;())).&lt;/span&gt;&lt;span class="na"&gt;thenAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getArgument&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="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Domain&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;getService&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingDomain&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="n"&gt;updateDomain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;final&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;Domain&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;captor&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;times&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="na"&gt;update&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="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;persisted&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="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingDomain&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="n"&gt;persisted&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="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persisted&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCreatedById&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="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persisted&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUpdatedById&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="n"&gt;existingDomain&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="n"&gt;result&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getUpdateTransformer().copyToOutput(updateDomain, domain)&lt;/code&gt; is supposed to copy only the fields the update DTO actually carries - name, SKU, price, dimensions onto the loaded domain object. Nothing about &lt;code&gt;UpdateProduct&lt;/code&gt; or &lt;code&gt;UpdateCategory&lt;/code&gt; mentions &lt;code&gt;createdById&lt;/code&gt;. But "the DTO doesn't carry the field" and "the transformer definitely won't touch the field" are two different guarantees, and only one of them is enforced by the type system. The shared transformer base class both &lt;code&gt;UpdateProductTransformer&lt;/code&gt; and &lt;code&gt;UpdateCategoryTransformer&lt;/code&gt; extend is exactly the kind of shared code where a well-intentioned future change could add a line that resets audit fields as a default, on the reasoning that every update should reset something. If that line ever landed, every existing test that only asserts on name, SKU, and price would keep passing, because none of them look at &lt;code&gt;createdById&lt;/code&gt; at all. The test doesn't exist because I distrust today's transformer. It exists because the transformer is shared ancestry, and shared ancestry is exactly where a mistake gets inherited by every subclass at once - the same lesson the opening story already taught the hard way.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;loadById()&lt;/code&gt; and &lt;code&gt;delete()&lt;/code&gt; follow the same authorization pattern as &lt;code&gt;update()&lt;/code&gt;, with one deliberate asymmetry worth calling out: &lt;code&gt;delete()&lt;/code&gt; is idempotent by design, established back in Chapter 9 on RFC 7231 grounds, and the test proving it looks almost suspiciously simple:&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;delete_WhenEntityDoesNotExist_ShouldBeANoOp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;randomId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomId&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="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;assertDoesNotThrow&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;getService&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&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;getMockDao&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;times&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="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;randomId&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;getMockDao&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;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&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;There's no exception to catch, no return value to inspect, the entire assertion is behavioral: the DAO gets asked to look, and is never asked to delete something that was never there. It's a small test, and it's easy to underrate exactly because it's small. A previous version of this same test verified &lt;code&gt;dao.create()&lt;/code&gt; was never called instead of &lt;code&gt;dao.update()&lt;/code&gt; - the wrong method entirely, copy-pasted down from a sibling test and never corrected. It would have passed forever regardless of what &lt;code&gt;delete()&lt;/code&gt; actually did, because nothing it checked had anything to do with deletion. The fix wasn't clever, it was reading the test's own name against its own assertions and noticing they'd stopped agreeing with each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  BigDecimal, and the failure that only shows up later
&lt;/h2&gt;

&lt;p&gt;One more small decision belongs in this section, not because it's exotic, but because it's the kind of mistake that's genuinely invisible until it produces a wrong invoice in production. &lt;code&gt;BigDecimal.equals()&lt;/code&gt; considers scale part of the value being compared - &lt;code&gt;new BigDecimal("239.99").equals(new BigDecimal("239.990"))&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;, despite both representing the identical monetary amount. A field-mapping test asserting &lt;code&gt;assertEquals(newDto.getPrice(), result.getPrice())&lt;/code&gt; would be correct today, while the DTO and the domain object happen to construct their &lt;code&gt;BigDecimal&lt;/code&gt; values with matching scale, and would become a source of mysterious, intermittent test failures the moment either side's construction path changed - a different &lt;code&gt;BigDecimal.valueOf(...)&lt;/code&gt; overload, a database column that returns a different scale than the one supplied. Every price assertion in this suite uses &lt;code&gt;compareTo() == 0&lt;/code&gt; instead, because that's the only question that actually matters for money: are these the same amount, not are these represented identically.&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="n"&gt;assertEquals&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="n"&gt;newDto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;compareTo&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="na"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting this wrong in a chapter about testing discipline would have been its own small failure of the thing the chapter argues for.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's inherited, and what isn't yet
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AbstractCrudServiceTestCase&lt;/code&gt; now does exactly what it was supposed to: every guard, every ownership rule, every authorization path for &lt;code&gt;create()&lt;/code&gt;, &lt;code&gt;update()&lt;/code&gt;, &lt;code&gt;loadById()&lt;/code&gt;, and &lt;code&gt;delete()&lt;/code&gt; is written once, and any CRUD service that extends &lt;code&gt;AbstractCrudService&lt;/code&gt; picks all of it up without writing a line. &lt;code&gt;ProductsServiceTestCase&lt;/code&gt; and &lt;code&gt;CategoriesServiceTestCase&lt;/code&gt; prove it by how little they have to add on top - field mapping through their own transformer, and nothing else that this generic contract already covers.&lt;/p&gt;

&lt;p&gt;But "nothing else that this generic contract covers" is doing some quiet work in that sentence, because there's a whole method -  &lt;code&gt;changeStatus()&lt;/code&gt; sitting on both concrete services that the abstract suite never touches at all, and it's worth asking why before assuming it's an oversight.&lt;/p&gt;

&lt;p&gt;Part 2 picks up exactly there: why &lt;code&gt;changeStatus()&lt;/code&gt; was deliberately left out of the shared ancestor, a coverage gap that survived hiding inside a fully-executed branch, and one more pass through the test suite itself to find a redundancy that had no business surviving this long.&lt;/p&gt;




&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 10, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-10-bl-testing&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-10-bl-testing" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/the-core-business-logic-module-chapter-9-iek"&gt;Read Chapter 9: The Core Business Logic Module&lt;/a&gt;&lt;br&gt;
▶️ Read Chapter 10 - Part 2: Testing the Service Layer: Where the Shared Ancestor Ends (Coming soon)&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>programming</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
    <item>
      <title>Our H2 tests passed but postgres in production disagreed</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 14 Sep 2026 07:20:09 +0000</pubDate>
      <link>https://dev.to/kamenivanov/our-h2-tests-passed-but-postgres-in-production-disagreed-4lca</link>
      <guid>https://dev.to/kamenivanov/our-h2-tests-passed-but-postgres-in-production-disagreed-4lca</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnjvucu3n6x75h8ohegmt.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnjvucu3n6x75h8ohegmt.jpeg" alt="Cover" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The test suite was green, as it had been for months. The migration ran clean on H2, the repository tests passed, the CI pipeline gave its usual thumbs up, and the change went out. A few hours later, a batch job that touched category records started failing in production with an error none of us recognized on sight: invalid input value for enum category_status. We didn't have that enum in our test environment. We had a &lt;code&gt;VARCHAR&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;The status field had gone through a refactor earlier in the year, moving from a loose two-value flag to a proper three-value enum, &lt;code&gt;ACTIVE&lt;/code&gt;, &lt;code&gt;INACTIVE&lt;/code&gt;, &lt;code&gt;ARCHIVED&lt;/code&gt;. On Postgres, that was implemented as a native enum type at the database level, which is the stricter, more correct way to do it, since the database itself now refuses anything outside the three allowed values. On H2, for reasons that made sense at the time and stopped making sense the day this happened, the equivalent migration script defined the column as a plain string. H2 doesn't support native Postgres enum types, so somewhere along the way the two migration scripts had quietly diverged, one for tests, one for production, and nobody had flagged it as a problem because both passed their respective test suites.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0v3xdcv496auqr2cgb5o.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0v3xdcv496auqr2cgb5o.png" alt="H2 vs Postgress difference" width="680" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The batch job was writing a leftover status value from before the refactor, a string that used to be valid and no longer was. On H2, that string went into a &lt;code&gt;VARCHAR&lt;/code&gt; column without complaint. On Postgres, the database did exactly what a native enum type is supposed to do and rejected it outright. The bug wasn't really in the batch job. It was in the fact that our tests had never once run against a schema that could actually catch this class of error, because the schema they ran against wasn't the same schema running in production.&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="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;03&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;07.113&lt;/span&gt; &lt;span class="no"&gt;ERROR&lt;/span&gt; &lt;span class="o"&gt;---&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SqlExceptionHelper&lt;/span&gt;
&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;postgresql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PSQLException&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;ERROR:&lt;/span&gt; &lt;span class="n"&gt;invalid&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nl"&gt;category_status:&lt;/span&gt; &lt;span class="s"&gt;"LEGACY_HIDDEN"&lt;/span&gt;
&lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="nc"&gt;CategoryStatusUpdateBatchJob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;processRecord&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategoryStatusUpdateBatchJob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;java&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hibernate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SQLStateConversionDelegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;
&lt;span class="no"&gt;H2&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="nl"&gt;job:&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="no"&gt;PASSED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We moved that test suite onto Testcontainers, running the real Postgres migration script instead of an H2 substitute, and reran the batch job's test coverage against it. It failed on the first run. Ten minutes of investigation, and it was pointing at the exact same invalid enum value production had hit, except this time in a local test run instead of an incident channel.&lt;/p&gt;

&lt;p&gt;The lesson isn't that H2 is bad, it's fast and convenient for a lot of testing. The lesson is that convenience has a boundary, and that boundary is anywhere your production database enforces something your test database doesn't know how to enforce. If your schema does real work at the database level, constraints, enums, exclusion rules, your tests need to run against a database willing to do that same work. Otherwise green tests are only telling you the code runs, not that it's correct.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;More like this?&lt;/strong&gt;&lt;br&gt;
I write short production war stories like this one, plus a deep-dive architecture series. &lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;Subscribe here&lt;/a&gt; if you want both in your inbox.&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>One Missing Parameter Doubled Our Connection Pool Wait Time</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:47:15 +0000</pubDate>
      <link>https://dev.to/kamenivanov/one-missing-parameter-doubled-our-connection-pool-wait-time-30ag</link>
      <guid>https://dev.to/kamenivanov/one-missing-parameter-doubled-our-connection-pool-wait-time-30ag</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnfsfnq01pb2xarj42n9e.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnfsfnq01pb2xarj42n9e.jpeg" alt="Cover" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
It was a Friday afternoon, the worst possible time for an incident, which is of course when it happened. Traffic was at its weekly peak, and our connection pool metrics started climbing in a way that didn't match the load. Requests weren't failing outright, they were just queuing, waiting longer and longer for a connection to free up. Nothing in the code had changed that week. Nothing in the infrastructure had changed either. The only thing that had changed was traffic volume, and the pool was buckling under it far earlier than our capacity planning said it should. The investigation started where these things usually start, with dashboards and guesses. CPU was fine. Database load was fine. The pool itself was correctly sized for what we thought our read-to-write ratio was. That assumption turned out to be the problem.&lt;/p&gt;

&lt;p&gt;A handful of endpoints, all of them read-only by contract, category listings, product lookups, dashboard summaries, were annotated with plain &lt;code&gt;@Transactional&lt;/code&gt;, missing the &lt;code&gt;readOnly = true&lt;/code&gt; attribute. They worked and they returned the correct data. Nothing about their behavior looked wrong in any functional test. But without that attribute, Hibernate treated every one of those calls as a potential write, It tracked every loaded entity for dirty checking, it ran a flush at the end of the transaction to look for changes to persist, and none of that work is free. On a single request it costs a few milliseconds nobody notices but multiply it across the volume of read traffic we get at peak, and connections were being held measurably longer than they needed to be, which meant fewer of them were cycling back into the pool for the next request in line.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7rpjzf0mqw2oj9e5iuj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7rpjzf0mqw2oj9e5iuj.png" alt="Before and After the fix comparison" width="556" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix was easy enough - adding &lt;code&gt;readOnly = true&lt;/code&gt; to those methods took an afternoon. Hibernate skips the dirty check, skips the flush, and the transaction closes as soon as the data is read. Pool wait times at the next peak dropped by roughly half, which lines up with how much of our traffic was hitting those endpoints in the first place.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4adna9kk44y4exhtrmx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4adna9kk44y4exhtrmx.png" alt="Connection Pool flow" width="680" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What stuck with me afterward wasn't the fix, it was how invisible the cost was until the volume made it visible. &lt;code&gt;readOnly = true&lt;/code&gt; reads like a hint, almost decorative, the kind of thing that's easy to skip when you're moving fast and the tests are green but it isn't decorative. It's telling the persistence layer to stop doing work it was never going to need, and at scale that work adds up to real contention on a resource every request in your system is competing for.&lt;/p&gt;

&lt;p&gt;If you're auditing your own services for this, the search is quick: grep for &lt;code&gt;@Transactional&lt;/code&gt; without &lt;code&gt;readOnly&lt;/code&gt;, then check whether the method actually writes anything. The attribute costs a few keystrokes but the absence of it costs connections you'll be short of exactly when you can least afford it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;More like this?&lt;/strong&gt;&lt;br&gt;
I write short production war stories like this one, plus a deep-dive architecture series. &lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;Subscribe here&lt;/a&gt; if you want both in your inbox.&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>jpa</category>
      <category>hibernate</category>
    </item>
    <item>
      <title>The Core Business Logic Module (Chapter 9)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 07 Sep 2026 09:43:43 +0000</pubDate>
      <link>https://dev.to/kamenivanov/the-core-business-logic-module-chapter-9-iek</link>
      <guid>https://dev.to/kamenivanov/the-core-business-logic-module-chapter-9-iek</guid>
      <description>&lt;p&gt;A business layer is only as independent as the shape of the objects that cross its boundary. It doesn't matter how carefully a service class avoids importing &lt;code&gt;javax.servlet&lt;/code&gt; or a Kafka client if the object it accepts as input is a one-to-one mirror of the database row. At that point the transport has already leaked in, just wearing a plain Java object instead of a &lt;code&gt;@RequestBody&lt;/code&gt; annotation. The rule this chapter works from is narrow enough to apply consistently: a service method's input should reflect what a caller is allowed to &lt;em&gt;do&lt;/em&gt;, not what the entity happens to &lt;em&gt;look like&lt;/em&gt;. Creating a product and updating one are different operations with different rules about what's mutable, who sets what, and what's off-limits entirely - so they get different input types, even when large parts of them overlap. The same goes for who's asking: a &lt;code&gt;requesterId&lt;/code&gt; arrives as a bare &lt;code&gt;UUID&lt;/code&gt;, and the service has no idea about whether it came from a JWT claim, a session, or a Kafka message header. That indifference is the actual independence being aimed for, not the absence of framework imports.&lt;/p&gt;

&lt;p&gt;What makes this rule worth a full chapter, rather than a paragraph, is the case where following it isn't obvious, where a field looks like it belongs on one side of the boundary until you actually reason through what the operation is supposed to guarantee.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "independent" is supposed to mean
&lt;/h2&gt;

&lt;p&gt;"Business logic should be independent of transport and infrastructure" gets repeated often enough that it stops meaning anything. In practice, for this service layer, it means three concrete things:&lt;/p&gt;

&lt;p&gt;The service methods don't know whether the caller is a REST controller, a Kafka listener, or a CLI tool. They take a &lt;code&gt;requesterId&lt;/code&gt; as a &lt;code&gt;UUID&lt;/code&gt; and a domain-shaped input, and that's the entire contract. Nothing about how that UUID was extracted - JWT claim, session or a message header.&lt;/p&gt;

&lt;p&gt;The service methods don't know what database sits behind the DAO. &lt;code&gt;AbstractCrudService&lt;/code&gt; depends on &lt;code&gt;CrudDao&amp;lt;UUID, Domain&amp;gt;&lt;/code&gt;, an interface. Whether that's MySQL, an in-memory test double, Apache Coherence or something else entirely is invisible from here.&lt;/p&gt;

&lt;p&gt;And the service methods don't accept whatever shape of data happens to be convenient for the caller. They accept exactly the shape that matches what the operation is allowed to do. That third point is where status transitions turn out to be a harder case than they first look, so it's worth slowing down on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Different intent, different object
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NewCategory&lt;/code&gt; carries just a name - a category is created without a provided status at all, it's &lt;code&gt;INACTIVE&lt;/code&gt; by default until someone deliberately changes that. &lt;code&gt;UpdateCategory&lt;/code&gt; carries a name too, because renaming a category turned out to be a legitimate, unremarkable operation with no special rule attached to it. What it doesn't carry is status, and that absence is the interesting part:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateCategory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// no status field&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not the same reasoning as leaving a field off because it's immutable, name isn't immutable here, it's just not entangled with anything else. Status is left off for the opposite reason: changing it &lt;em&gt;is&lt;/em&gt; entangled with a rule, the same kind &lt;code&gt;Product&lt;/code&gt; enforces through &lt;code&gt;transitionTo&lt;/code&gt;. A category can move from &lt;code&gt;INACTIVE&lt;/code&gt; to &lt;code&gt;ACTIVE&lt;/code&gt; or straight to &lt;code&gt;ARCHIVED&lt;/code&gt;, but once &lt;code&gt;ACTIVE&lt;/code&gt; it can only be archived, and &lt;code&gt;ARCHIVED&lt;/code&gt; is terminal - no path back. None of that belongs in a generic update method next to a name change, so it doesn't live there. It lives in its own operation:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;changeStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CategoryStatus&lt;/span&gt; &lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loadOrThrowNotFound&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;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;authorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUpdatedById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&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;This category originally shipped with a plain &lt;code&gt;active&lt;/code&gt; boolean, and for a while that was a perfectly reasonable representation, there were exactly two states, and a flag models two states fine. The same reasoning that shaped &lt;code&gt;ProductStatus&lt;/code&gt; into a guarded enum applied here too, once a third state showed up: a boolean has no way to express "archived" without either overloading &lt;code&gt;active = false&lt;/code&gt; to mean two different things, or bolting on a second flag and hoping the combination of both flags never goes somewhere nonsensical. The category status shows that lesson getting applied a second time, ahead of the problem this time rather than after it, a &lt;code&gt;CategoryStatus&lt;/code&gt; enum with the same &lt;code&gt;canTransitionTo&lt;/code&gt; shape as &lt;code&gt;ProductStatus&lt;/code&gt;, so a name change and a status change stay two operations that can never be confused for one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NewProduct&lt;/code&gt; and &lt;code&gt;UpdateProduct&lt;/code&gt; follow the same instinct for most fields, no caller ever gets to set &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;createdAt&lt;/code&gt;, or &lt;code&gt;createdById&lt;/code&gt; from outside, because those fields don't exist on either input object. They're populated by the service (&lt;code&gt;preProcessNewEntity&lt;/code&gt;) or never populated by input at all. A create payload and the domain object it produces are not the same shape, on purpose, and that gap is the whole point: it's not possible to forge a creation timestamp or claim someone else's authorship, because there's no field to put it in. The same discipline shows up in how &lt;code&gt;UpdateProductTransformer&lt;/code&gt; handles the embedded specification, and it's a smaller but no less deliberate decision:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;()&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="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSpecification&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;ProductSpecification&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;getDimensions&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;getWeight&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;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setDimensions&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;getDimensions&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpecification&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setWeight&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;getWeight&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;An update caller sends flat &lt;code&gt;dimensions&lt;/code&gt; and &lt;code&gt;weight&lt;/code&gt; fields, not a &lt;code&gt;ProductSpecification&lt;/code&gt; object. The transformer decides whether that means constructing a new embedded object or mutating an existing one, and the caller never has to know or care which branch ran. That's the input shape matching the &lt;em&gt;operation&lt;/em&gt;, not the internal object graph, a product created without dimensions can still be updated to add them later, without the caller needing to understand that &lt;code&gt;ProductSpecification&lt;/code&gt; exists as a separate type at all.&lt;/p&gt;

&lt;p&gt;Status is where that question gets harder to answer by instinct, and it's worth reasoning through it explicitly. At first glance, &lt;code&gt;status&lt;/code&gt; looks like it belongs on &lt;code&gt;UpdateProduct&lt;/code&gt; next to name, SKU, and price, it's a field on the entity, the caller wants to change it, so it goes in the update payload with the rest. That reasoning holds right up until you ask what "changing status" is actually supposed to mean, and the domain object itself answers that question before the DTO gets a chance to:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;nextStatus&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;canTransitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextStatus&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Business rule violated: Cannot transition from "&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;status&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" to "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;nextStatus&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextStatus&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;code&gt;Product&lt;/code&gt; doesn't expose a &lt;code&gt;setStatus&lt;/code&gt;. It exposes a guarded transition. There's no legal way to skip from &lt;code&gt;DRAFT&lt;/code&gt; straight to &lt;code&gt;ARCHIVED&lt;/code&gt;, or to move backward from &lt;code&gt;PUBLISHED&lt;/code&gt; to &lt;code&gt;DRAFT&lt;/code&gt;, without the domain itself refusing. That's the correct place for that rule to live - not in a controller, not in a transformer, not scattered across whichever service happens to touch the domain next. Which means a plain field copy in the transformer - &lt;code&gt;product.setStatus(dto.getStatus())&lt;/code&gt; was never going to be the right move, regardless of whether anyone remembered to write it. It would have bypassed &lt;code&gt;canTransitionTo&lt;/code&gt; entirely, applying whatever status the caller sent without checking if the move was legal. Once that's clear, the field doesn't belong on &lt;code&gt;UpdateProduct&lt;/code&gt; at all, not because leaving it there caused a defect, but because a generic update method has no business deciding whether a status transition is allowed. That's a different kind of operation, with its own rule, and it earns its own method - the same one &lt;code&gt;Category&lt;/code&gt; already uses:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;changeStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loadOrThrowNotFound&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;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;authorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUpdatedById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;It's worth noticing that this is the exact same shape as &lt;code&gt;Category.changeStatus&lt;/code&gt; - same load, same authorize call, same transition, same save. Neither entity needed a bespoke &lt;code&gt;publish()&lt;/code&gt; or &lt;code&gt;activate()&lt;/code&gt; method with its own hand-rolled orchestration. The variation between products and categories lives entirely inside &lt;code&gt;canTransitionTo&lt;/code&gt;, where it belongs, not in how the operation is wired up around it.&lt;/p&gt;

&lt;p&gt;Now there are three independent layers standing between a caller and an invalid state: the DTO doesn't carry the field at all, the service exposes the change as a named, intentional action, and the domain object enforces the transition rule regardless of who calls it. Any one of those layers failing doesn't compromise the other two. That's worth more than a single well-placed validation, because it's the kind of protection that survives someone else touching the code six months from now without having read this chapter.&lt;/p&gt;

&lt;p&gt;The same status reasoning extends one layer further, past persistence. &lt;code&gt;CategoryStatus&lt;/code&gt; - the domain enum with &lt;code&gt;canTransitionTo&lt;/code&gt; and &lt;code&gt;CategoryStatusEntity&lt;/code&gt; the JPA-mapped enum stored as a string column are two separate types, converted explicitly at the DAO boundary rather than shared directly. The domain module never imports anything from &lt;code&gt;jakarta.persistence&lt;/code&gt;, so it has no idea &lt;code&gt;EnumType.STRING&lt;/code&gt; exists, or that the column is even a string rather than an integer code. That's the same independence argument as the transport boundary, aimed at the other side of the service: the business rule for what states a category can move through doesn't change if the storage representation does.&lt;/p&gt;




&lt;h2&gt;
  
  
  The template, and what it deliberately doesn't decide
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AbstractCrudService&lt;/code&gt; is the same shape for both &lt;code&gt;ProductsServiceImpl&lt;/code&gt; and &lt;code&gt;CategoriesServiceImpl&lt;/code&gt;, and that repetition is intentional rather than lazy. &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;loadById&lt;/code&gt;, and &lt;code&gt;delete&lt;/code&gt; are implemented once, in the base class, calling out to a small set of abstract hooks:&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="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nc"&gt;Transformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CreateDomain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getCreateTransformer&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nc"&gt;Transformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UpdateDomain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUpdateTransformer&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each concrete service answers three questions - how do I build this domain object from a create payload, how do I apply an update payload, and who's allowed to touch this domain object and the orchestration around those answers (transaction boundaries, null checks, the not-found and unauthorized exceptions) is written exactly once.&lt;/p&gt;

&lt;p&gt;It's worth addressing the one place this chapter's independence argument looks like it bends: every write method carries &lt;code&gt;@Transactional&lt;/code&gt;, a Spring annotation, sitting directly in what's supposed to be framework-agnostic business logic. That's a smaller compromise than it looks. &lt;code&gt;@Transactional&lt;/code&gt; is declarative metadata, read reflectively by a proxy Spring builds around the class - the method itself never touches a &lt;code&gt;TransactionManager&lt;/code&gt;, never begins or commits anything, never knows the annotation is even being honored. Strip Spring from the classpath and the method still compiles and runs, just without the transactional wrapping. That's a fundamentally different kind of coupling than a JPA entity or an &lt;code&gt;HttpServletRequest&lt;/code&gt; parameter would be, where the method literally cannot execute without the framework type on the classpath.&lt;br&gt;
The alternative - moving transaction demarcation out to the controller, or configuring it externally via XML or AspectJ doesn't remove the framework, it just relocates a decision that belongs here anyway: what counts as one atomic unit of work for a given operation is a business question, not a transport one. A controller has no basis for deciding whether &lt;code&gt;update()&lt;/code&gt; should be one transaction or two. That's exactly why this rule is enforced at exactly one layer. &lt;code&gt;@Transactional&lt;/code&gt; lives on service methods, in this base class or its concrete implementations, and nowhere else. Not on controllers, not inside DAO implementations. A transaction boundary that can start from either direction is a transaction boundary nobody can reason about.&lt;/p&gt;

&lt;p&gt;Notice also what &lt;code&gt;Transformer&amp;lt;CreateDomain, Domain&amp;gt;&lt;/code&gt; doesn't do: it only moves data one direction, from the create or update input object into the domain object it produces. There's no &lt;code&gt;copyToInput&lt;/code&gt; here for turning that domain object back into a response shape, that's a different interface entirely, used elsewhere in the codebase for DTO serialization. Collapsing the two into one bidirectional transformer would be a natural shortcut, since a lot of the field-copying code would look identical going either direction. It would also mean the same class that decides how a creation payload becomes a product is now also deciding how a product becomes an API response and the moment those two responsibilities share a class, it becomes easy to add a field to one direction and forget the other silently breaks the boundary this chapter is about. Keeping create/update transformers strictly one-way means that possibility doesn't exist structurally, not just by convention.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;authorize&lt;/code&gt; is worth pausing on, because it takes the fully loaded domain object, not just an id:&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="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;requesterId&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCreatedById&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requesterId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthorizationException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UNAUTHORIZED&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;That forces a load before the authorization check can run, which looks like it could be optimized away. It can't, not without losing generality. Ownership by creator works for products, but nothing guarantees the next domain object's access rule is that simple, it could depend on a collaborator list, a tenant boundary, a status field, any combination of the object's own state. The moment authorization can depend on arbitrary domain content, loading the object first stops being a choice and becomes a requirement. The base class doesn't try to special-case this, it just accepts the domain object as a parameter and lets each subclass decide what "authorized" means for that domain.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;delete&lt;/code&gt; carries a smaller, quieter version of the same idea:&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="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;domain&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="k"&gt;return&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;Deleting something that doesn't exist isn't an error here - it's a no-op. That's a defensible, idempotent design, and it matches how HTTP itself defines the semantics of DELETE: RFC 7231 specifies DELETE as idempotent, meaning repeated identical requests should leave the system in the same end state as a single one - a resource that's already gone stays gone, without that repetition needing to be an error. It's also easy to violate by accident. When someone adds anything after &lt;code&gt;dao.delete()&lt;/code&gt; in this method - an audit entry, a notification, anything - that addition needs to remember this early return exists, or it'll fire for deletes that never happened. Worth a comment at the call site, not because the current code is wrong, but because the next change to it won't have this context unless it's written down.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this buys, concretely
&lt;/h2&gt;

&lt;p&gt;None of this is abstract payoff. A &lt;code&gt;ProductsServiceImpl&lt;/code&gt; constructed with nothing but a &lt;code&gt;ProductDao&lt;/code&gt; can be exercised with a mocked DAO and zero Spring context - no database, no HTTP layer, no message broker, because none of those things are reachable from inside the class in the first place. The transformer contract only moves data one direction, so there's no path by which a create or update payload construction accidentally becomes the shape used for API responses later. And status transitions, once reasoned through as their own kind of operation rather than just another field, end up structurally incapable of bypassing the domain object's own rules, not because someone remembered to guard against it, but because the shortcut was never given a path to exist.&lt;/p&gt;

&lt;p&gt;The same boundary is what makes future changes cheap instead of invasive. If &lt;code&gt;ProductDao&lt;/code&gt; moves from MySQL to something else, or gets a caching layer bolted in front of it, &lt;code&gt;ProductsServiceImpl&lt;/code&gt; doesn't change, it was never coupled to the implementation, only the interface. The same will hold when this service eventually needs to notify the rest of the system that a product was created or updated, that's a dependency the service will accept through its constructor, the same way it accepts the DAO now, and the create and update methods above won't need to know whether that notification goes out over Kafka directly or through an outbox table written in the same transaction. That's a later chapter's problem specifically because this chapter's boundary makes it one.&lt;/p&gt;

&lt;p&gt;The tests that prove all of this - what a mocked DAO actually looks like, how you assert an unauthorized update throws before it touches persistence, how you write a test that locks in a status transition rule so a future change can't quietly loosen it are their own chapter. This one was about the shape the code has to have before those tests are worth writing.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;Chapter 10 walks through testing this exact service layer with the DAO mocked out entirely, no Spring context, no database, no HTTP layer, using &lt;code&gt;ProductsServiceImpl&lt;/code&gt; and &lt;code&gt;CategoriesServiceImpl&lt;/code&gt; as the running examples. The boundary this chapter drew is what makes those tests possible in the first place - the next one is about actually writing them, including the test that would have caught the status field slipping through if it had shipped that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 9, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-09-business-logic&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-09-business-logic" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/why-almost-every-temporary-workaround-is-permanent-chapter-8-2on5"&gt;Read Chapter 8: Why almost every ‘Temporary’ Workaround is Permanent&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/testing-the-service-layer-part-1-what-the-generic-suite-owes-every-service-chapter-10-2ok4"&gt;Read Chapter 10: The Testing of Business Logic Service&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Why almost every ‘Temporary’ Workaround is Permanent (Chapter 8)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 31 Aug 2026 05:55:42 +0000</pubDate>
      <link>https://dev.to/kamenivanov/why-almost-every-temporary-workaround-is-permanent-chapter-8-2on5</link>
      <guid>https://dev.to/kamenivanov/why-almost-every-temporary-workaround-is-permanent-chapter-8-2on5</guid>
      <description>&lt;p&gt;Somewhere around month four of any project I've worked on, someone says a version of the same sentence. &lt;em&gt;"Just hardcode it for now, we'll fix it after the release."&lt;/em&gt; Nobody arguing against it and they genuinely believe it's temporary. The problem isn't the shortcut itself, sometimes a shortcut is the correct call. The problem is that "temporary" has no owner, no ticket, and no deadline of its own. It just sits there until someone forgets it was ever supposed to be temporary at all, and by then it's load-bearing. Here's how that actually plays out, because the abstract version of this story is boring and everyone nods along without changing anything.&lt;/p&gt;




&lt;h3&gt;
  
  
  The shortcut
&lt;/h3&gt;

&lt;p&gt;We needed to ingest pricing updates from an upstream provider. Three days, full stop, because a partner integration deadline had already been communicated externally and nobody wanted to be the one calling to push it back. The fastest path was obvious: inject a &lt;code&gt;RestClient&lt;/code&gt; call directly into the business-logic service method that needed the data, parse the JSON inline, map a couple of fields by hand, and move on.&lt;/p&gt;

&lt;p&gt;We shipped it on time, it worked and everyone moved on to the next fire, which is what always happens after a deadline shortcut lands successfully. Success is exactly what makes it invisible.&lt;/p&gt;

&lt;p&gt;Two months later we get an update: "The upstream team is moving to Kafka. They want to push events, not serve a REST endpoint anymore.", and now the HTTP call is gone, but it was never just a call - it was call semantics baked into a domain-facing method signature. The service method that used to return synchronously now needs to react to a stream. Unit tests that mocked an &lt;code&gt;HttpClient&lt;/code&gt; are gone and now they need to mock a Kafka consumer instead. Every call site that invoked the pricing method synchronously has to be reconsidered, because the whole shape of "when do we have this data" changed underneath them.&lt;/p&gt;

&lt;p&gt;Three weeks after that migration finished, we got again another update: &lt;em&gt;"Edge devices in the field can't maintain a Kafka connection reliably. We're pushing over MQTT instead."&lt;/em&gt; Same story again, except now it's the third rewrite of the same core logic in five months, and each rewrite touches more of the codebase than the last one did, because more things had grown to depend on the leaked transport assumptions in the meantime.&lt;/p&gt;

&lt;p&gt;Nobody planned for three transport migrations. Nobody ever does. That's exactly the point - the shortcut wasn't wrong because REST was the wrong choice, it was wrong because it welded a decision that was going to change into logic that was supposed to be stable.&lt;/p&gt;




&lt;h3&gt;
  
  
  What it should have looked like
&lt;/h3&gt;

&lt;p&gt;This is precisely why &lt;code&gt;business-logic&lt;/code&gt; in this project never talks to a transport mechanism directly. It depends on an interface, a port, if you want the textbook word for it, though I'd rather just call it what it is: an interface that answers the domain's question ("give me the latest price for this SKU") without knowing or caring how the answer arrives. Whether that interface is backed by a REST client, a Kafka listener, or an MQTT subscriber is &lt;code&gt;bridge-impl&lt;/code&gt;'s problem, sealed away from &lt;code&gt;business-logic&lt;/code&gt; by the same compiler-enforced module boundary that's been the whole point of this architecture since Chapter 1.&lt;/p&gt;

&lt;p&gt;When the transport changes, &lt;code&gt;bridge-impl&lt;/code&gt; changes. The interface in &lt;code&gt;bridge-api&lt;/code&gt; doesn't move, and neither does a single line in &lt;code&gt;business-logic&lt;/code&gt;. Three transport migrations in five months would have been three rewrites of one implementation of the interface instead of three rewrites of everything downstream of a leaked implementation detail.&lt;/p&gt;

&lt;p&gt;The uncomfortable part of this story is that we didn't have that boundary in place when the first shortcut happened. We built it &lt;em&gt;because&lt;/em&gt; of this exact pain, not before it. That's usually how it goes, the boundary gets built in the shape of whatever already went wrong once, the same way most safety rules only exist because a crash already happened.&lt;/p&gt;




&lt;h3&gt;
  
  
  The same failure, dressed differently
&lt;/h3&gt;

&lt;p&gt;The transport story is the clean, satisfying version, because it has a name and a fix. The uglier version of the same failure shows up whenever someone decides a mapper is overhead the deadline can't afford. &lt;code&gt;"Why write a DTO and a transformer? Just return the entity from the controller."&lt;/code&gt; It takes five minutes to wire up and it works immediately, which is exactly the illusion - the cost isn't paid at the point of the shortcut, it's paid the first time someone needs to rename a column and discovers that half a dozen frontend clients have quietly come to depend on the JPA entity's field names as if they were a public contract.&lt;/p&gt;

&lt;p&gt;This is what the &lt;code&gt;Transformer&lt;/code&gt;/&lt;code&gt;BiTransformer&lt;/code&gt; split from Chapter 4 actually buys you, beyond the argument against MapStruct. It's not really about avoiding a mapping library, It's about there being exactly one place where "how the database shape becomes the domain shape" is decided, so that decision can change without becoming an incident. Skip it under deadline pressure and you haven't saved the mapping work, you've just deferred it to whoever eventually has to reverse-engineer which fields are safe to touch.&lt;/p&gt;




&lt;h3&gt;
  
  
  The actual cost, after the fact
&lt;/h3&gt;

&lt;p&gt;We went back and counted, roughly, what the three transport rewrites cost against what the port/adapter boundary would have cost to build up front. Building the boundary correctly the first time would have added maybe a half day or a day to the original three-day deadline - one interface, one adapter implementation, tests against the interface instead of the HTTP client. The three subsequent rewrites, combined, cost closer to three weeks, most of it not writing new code but tracing which parts of the business logic had quietly grown to assume synchronous, request-shaped delivery of data that was never going to stay synchronous or request-shaped.&lt;/p&gt;

&lt;p&gt;That ratio is the entire chapter. The shortcut doesn't remove the cost, it just moves it downstream, converts it from a schedule risk into a production risk, and hands the bill to whoever's on call when a customer reports that a price didn't update and nobody can immediately say which of three transport layers is currently responsible for getting it there.&lt;/p&gt;




&lt;h3&gt;
  
  
  The part that doesn't fit in a retro
&lt;/h3&gt;

&lt;p&gt;The hard part isn't spotting this pattern in hindsight, it's obvious once it's a war story. The hard part is that at the moment the shortcut gets proposed, it is correctly, the fastest way to hit the date. Refusing every shortcut on principle is its own failure mode. You'll miss real deadlines defending abstractions nobody needed. The distinction that actually matters is whether the shortcut cuts a corner &lt;em&gt;inside&lt;/em&gt; a module boundary you're allowed to redo later in isolation, or whether it cuts &lt;em&gt;through&lt;/em&gt; a boundary that other code is going to grow around before anyone circles back to it. A hardcoded value inside &lt;code&gt;bridge-impl&lt;/code&gt; is an annoyance, a hardcoded assumption smuggled into &lt;code&gt;business-logic&lt;/code&gt; about how data arrives is a liability with compound interest, and the interest rate is set by how many other developers touch that code before someone pays it down.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;Chapter 9 continues into the business logic module itself - what it means to write core services that depend on nothing but interfaces they own, and what actually breaks when that discipline slips.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 8. There's no code change between Chapter 7 and Chapter 8, I'm tagging each chapter regardless, for consistency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-08-shortcuts&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-08-shortcuts" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Maven 3.9.x and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/what-spring-data-tests-can-miss-testing-beyond-hibernates-persistence-context-chapter-7-5d7k"&gt;Read Chapter 7: The DAO Testing&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/the-core-business-logic-module-chapter-9-iek"&gt;Read Chapter 9: The Core Business Logic Module&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>software</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>What Spring Data Tests Can Miss: Testing Beyond Hibernate’s Persistence Context (Chapter 7)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 24 Aug 2026 07:41:04 +0000</pubDate>
      <link>https://dev.to/kamenivanov/what-spring-data-tests-can-miss-testing-beyond-hibernates-persistence-context-chapter-7-5d7k</link>
      <guid>https://dev.to/kamenivanov/what-spring-data-tests-can-miss-testing-beyond-hibernates-persistence-context-chapter-7-5d7k</guid>
      <description>&lt;p&gt;Here's the mechanism, before the war story: every &lt;code&gt;EntityManager&lt;/code&gt; keeps a first-level cache, an identity map of every entity it's touched in the current persistence context. Call &lt;code&gt;find()&lt;/code&gt; (which is what &lt;code&gt;loadById&lt;/code&gt; resolves to, directly or through Spring Data's derived &lt;code&gt;findById&lt;/code&gt;) for an entity that's already managed, and Hibernate doesn't ask the database anything. It just hands you back the object it already has. That's not a bug. It's the entire point of the first-level cache, and most of the time it's exactly the behavior you want. The problem is that a green &lt;code&gt;@DataJpaTest&lt;/code&gt; can’t tell the difference between 'I verified this round-trips through the database' and 'I got the same Java object reference back.' And I’ve watched teams ship code confident in tests that were only ever validating L1 cache state - right up until a missing constraint violation or a column mapping bug blew up in production because the data was never actually flushed and re-hydrated.&lt;/p&gt;

&lt;p&gt;In this chapter of the Evolutionary Architecture series, we're tearing down the illusion of the default Spring Data test. We'll look at exactly where the first-level cache hides bugs, and build a testing architecture around explicit &lt;code&gt;EntityManager&lt;/code&gt; clearing, generic test fixtures, and in-memory isolation - so a green test means what you think it means.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Standard Spring Tests Actually Test
&lt;/h3&gt;

&lt;p&gt;When you write a standard Spring Data test using &lt;code&gt;@DataJpaTest&lt;/code&gt; and a repository interface, your test looks clean and harmless:&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;testSaveProduct&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&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;Product&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Gaming Laptop"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;productDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;productDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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="n"&gt;assertNotNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loaded&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;"Gaming Laptop"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&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;It passes, but let's see here what actually happened:&lt;/p&gt;

&lt;p&gt;You call &lt;code&gt;create()&lt;/code&gt;. Hibernate generates a managed entity inside the persistence context - the row may or may not have hit the database yet, depending on flush mode. Then, even inside the &lt;code&gt;@Transactional&lt;/code&gt; wrapper the test runs in, calling &lt;code&gt;loadById()&lt;/code&gt; immediately after doesn't issue a &lt;code&gt;SELECT&lt;/code&gt;. The entity you just created is still sitting in the L1 cache, still attached to the same persistence context, and &lt;code&gt;find()&lt;/code&gt; short-circuits straight to it. You get the exact object reference back. The table itself never answered a query. To be precise about where this bites and where it doesn't: this specifically applies to &lt;code&gt;find()&lt;/code&gt; - based lookups. A custom JPQL or native &lt;code&gt;@Query&lt;/code&gt; method always issues real SQL, cache or no cache Hibernate will still try to reconcile the result with anything already managed, but the round-trip to the database happens either way. The gap is narrower than "all your reads are fake." It's specifically the &lt;code&gt;find()&lt;/code&gt;/&lt;code&gt;findById()&lt;/code&gt; path, which is also the one most CRUD test suites lean on hardest, because it's the one that requires the least code to write. So this test hasn't verified persistence. It's verified Hibernate's in-memory bookkeeping and Spring Data's proxy machinery which isn't nothing, but it's not what the test's name claims. A strict column constraint, a broken mapping, a query that only breaks against a real dialect - all of that can hide behind a green checkmark here.&lt;/p&gt;




&lt;h3&gt;
  
  
  Forcing a Real Write-and-Read Boundary
&lt;/h3&gt;

&lt;p&gt;If a test exists to verify persistence rather than domain behavior, it needs to force Hibernate to flush its write-behind queue to the database, then clear the persistence context before reading anything back. The next read has to hydrate a fresh entity graph from scratch - no shortcuts, no identity map hits.&lt;/p&gt;

&lt;p&gt;Doing that by hand in every test method is exactly the kind of boilerplate that gets skipped under deadline pressure, which defeats the point. So we push the discipline down into the test infrastructure itself, using a &lt;strong&gt;test-only proxy&lt;/strong&gt; that wraps every DAO and forces the flush-and-clear automatically after any write.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Architectural Boundary Note:&lt;/strong&gt; This proxy layer lives exclusively in test scope (&lt;code&gt;TestConfig&lt;/code&gt;, under &lt;code&gt;src/test/java&lt;/code&gt;). It's physically unreachable from production code and never ships in a production artifact. Production wiring injects the unproxied DAO implementations directly, so Hibernate keeps its normal write-behind batching with zero caching penalty outside of tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is our test-only &lt;code&gt;CrudDaoProxy&lt;/code&gt;:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CrudDaoProxy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;Dao&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;CrudDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Dao&lt;/span&gt; &lt;span class="n"&gt;proxied&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;EntityManager&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;CrudDaoProxy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dao&lt;/span&gt; &lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EntityManager&lt;/span&gt; &lt;span class="n"&gt;entityManager&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;proxied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proxied&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;entityManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;flushAndClear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;flushAndClear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="nf"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt; &lt;span class="n"&gt;id&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="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&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;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;flushAndClear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;flushAndClear&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Forces SQL out of write-behind cache into DB&lt;/span&gt;
        &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; 
        &lt;span class="c1"&gt;// Wipes L1 cache, forcing a real JDBC round-trip on next read&lt;/span&gt;
        &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&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;Notice what's deliberately &lt;em&gt;not&lt;/em&gt; overridden here: &lt;code&gt;loadById&lt;/code&gt; just delegates straight through to &lt;code&gt;proxied.loadById(id)&lt;/code&gt;, no flush-and-clear wrapper. It doesn't need one, by the time a test calls it, the previous write already forced the clear. Any DAO with search capability gets the same treatment one level up:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchableCrudDaoProxy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractParams&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SortBy&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Dao&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;SearchableDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudDaoProxy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Dao&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;SearchableDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;SearchableCrudDaoProxy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dao&lt;/span&gt; &lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EntityManager&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResultPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="n"&gt;criteria&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="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&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;code&gt;search()&lt;/code&gt; passes straight through too - it's backed by a &lt;code&gt;Specification&lt;/code&gt;, which always compiles to real SQL regardless of cache state, so there's nothing to force. The proxy only intervenes exactly where the shortcut exists: &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;. That asymmetry isn't an oversight, it's the whole design - wrap the three operations that can lie, leave the two that can't.&lt;/p&gt;

&lt;p&gt;A concrete feature DAO just extends the generic proxy and adds whatever custom query methods it has:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsDaoProxy&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SearchableCrudDaoProxy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;ProductSearchParams&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;ProductDao&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ProductDao&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;ProductsDaoProxy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductDao&lt;/span&gt; &lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EntityManager&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entityManager&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nf"&gt;loadBySku&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;sku&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="n"&gt;proxied&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadBySku&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&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;code&gt;loadBySku&lt;/code&gt; isn't part of the generic CRUD/search contract, so it gets a one-line override with no wrapping - same reasoning as &lt;code&gt;loadById&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Ground-Zero: The &lt;code&gt;TablesEraser&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Relying purely on Spring's &lt;code&gt;@Transactional&lt;/code&gt; rollback-per-test can still hide second-level cache pollution, batching side effects, or a connection leak that only shows up across tests, not within one.&lt;/p&gt;

&lt;p&gt;So every component test starts from an empty schema. &lt;code&gt;TablesEraser&lt;/code&gt; deletes every row from every table before each test runs:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TablesEraser&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;emptyAllTables&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;EntityManagerFactory&lt;/span&gt; &lt;span class="n"&gt;emf&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;DbQueryFactory&lt;/span&gt; &lt;span class="n"&gt;queryFactory&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;emptyAllTables&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emf&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queryFactory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;emptyAllTables&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;EntityManagerFactory&lt;/span&gt; &lt;span class="n"&gt;emf&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;DbQueryFactory&lt;/span&gt; &lt;span class="n"&gt;queryFactory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Predicate&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;filter&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;EntityManager&lt;/span&gt; &lt;span class="n"&gt;em&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createEntityManager&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTransaction&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;begin&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Disable foreign key checks dynamically&lt;/span&gt;
            &lt;span class="n"&gt;em&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createNativeQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disableForeignKeys&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeUpdate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Fetch all table names and truncate them&lt;/span&gt;
            &lt;span class="kd"&gt;final&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;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;allTableNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;em&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createNativeQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectAllTableNames&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResultList&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="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;allTableNames&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;filter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;em&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createNativeQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DELETE FROM "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeUpdate&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;// Re-enable foreign keys and commit&lt;/span&gt;
            &lt;span class="n"&gt;em&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createNativeQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableForeignKeys&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeUpdate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTransaction&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="n"&gt;th&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTransaction&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;rollback&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;th&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth being precise here: this is a &lt;code&gt;DELETE FROM&lt;/code&gt; per table, not a &lt;code&gt;TRUNCATE&lt;/code&gt;. That's a deliberate trade-off, not an oversight - &lt;code&gt;DELETE&lt;/code&gt; respects the same transaction we're already managing manually and rolls back cleanly if anything in the loop throws, where a bare &lt;code&gt;TRUNCATE&lt;/code&gt; in most dialects auto-commits and can't be undone mid-loop. It costs a little raw speed against a &lt;code&gt;TRUNCATE&lt;/code&gt;, and it's worth it for a test-suite reset that can fail safely.&lt;/p&gt;




&lt;h3&gt;
  
  
  Reusable Tests: &lt;code&gt;AbstractCrudTestCase&lt;/code&gt; &amp;amp; &lt;code&gt;AbstractSearchableTestCase&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Shared assertions cut real repetition, as long as the abstraction stays shallow. The moment a failing test sends you on a five-class inheritance hunt to understand what actually broke, the abstraction has stopped paying rent.&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractCrudTestCase&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="no"&gt;DAO&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractDaoTest&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;testCreate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="n"&gt;expectedEntity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runSave&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="n"&gt;dbEntity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedEntity&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="n"&gt;assertNotNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"missing entity"&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="n"&gt;expectedEntity&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="n"&gt;dbEntity&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="n"&gt;getAsserter&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;assertDeepEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbEntity&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;testCreateExisting&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runSave&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&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;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Entity existing!"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&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="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="no"&gt;DAO&lt;/span&gt; &lt;span class="nf"&gt;getDao&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="nf"&gt;createDomain&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nc"&gt;DeepEqualsAsserter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAsserter&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;A concrete case like &lt;code&gt;ProductDaoTestCase&lt;/code&gt; stays thin, it implements the domain-specific payload generator (&lt;code&gt;createDomain()&lt;/code&gt;) and inherits the full CRUD and search suite for free. It's not &lt;em&gt;only&lt;/em&gt; that, though. It also adds its own tests for &lt;code&gt;loadBySku&lt;/code&gt;, because that's a query method the generic contract has no way to know about. That's the right shape for this kind of abstraction - the base class owns everything that's structurally identical across every DAO, and the concrete case owns exactly the part that makes this DAO different. If a shared base class tried to guess at &lt;code&gt;loadBySku&lt;/code&gt; too, you'd be back to fighting a framework instead of using one.&lt;/p&gt;

&lt;p&gt;Not every test in this hierarchy exists to confirm the happy path. One of those tests is in the &lt;code&gt;AbstractSearchableTestCase&lt;/code&gt; that specifically is checking a boundary condition:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;testSearchNullParams&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;runSave&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createDomainWithUniqueData&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;  
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ResultPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&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;getDao&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;search&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;assertEquals&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="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;totalPages&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="mi"&gt;5&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="na"&gt;totalHits&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="mi"&gt;5&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="na"&gt;elements&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&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;That test earned its place in the suite the hard way. It didn't start out passing.&lt;/p&gt;




&lt;h3&gt;
  
  
  What the Null-Params Test Actually Caught
&lt;/h3&gt;

&lt;p&gt;The first version of &lt;code&gt;search()&lt;/code&gt; in &lt;code&gt;SearchableDaoImpl&lt;/code&gt; assumed a &lt;code&gt;Params&lt;/code&gt; object would always be there:&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="nf"&gt;toPage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="n"&gt;criteria&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;PageRequest&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;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSize&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;getSortOrDefault&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSortBy&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSortDirection&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;Reasonable assumption, right up until &lt;code&gt;testSearchNullParams&lt;/code&gt; called &lt;code&gt;getDao().search(null)&lt;/code&gt; and got a &lt;code&gt;NullPointerException&lt;/code&gt; on &lt;code&gt;criteria.getPage()&lt;/code&gt; instead of a result page. Which is exactly the point of writing that test before the feature felt finished - a caller passing &lt;code&gt;null&lt;/code&gt; to mean "no filters, give me the default page" is a completely ordinary thing to do, and the DAO was punishing it with a stack trace instead of handling it.&lt;/p&gt;

&lt;p&gt;The fix has two parts, and both matter for the same reason: &lt;code&gt;Params&lt;/code&gt; is a generic type parameter here, so there's no single field-by-field way to "default" a null one into existing. Every concrete DAO defines its own &lt;code&gt;Params&lt;/code&gt; subtype with its own fields, which means the null case has to be handled once, centrally, before any of that generic machinery gets involved - not pushed down into every feature DAO's &lt;code&gt;createSpecification&lt;/code&gt; implementation as a null-check they'd all have to remember to write.&lt;/p&gt;

&lt;p&gt;Paging falls back to a fixed default when there is no parameter to base it on:&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="nf"&gt;toPage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="n"&gt;criteria&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;criteria&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PageRequest&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="no"&gt;DEFAULT_PAGE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_SIZE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CREATED_AT_DESC_SORT&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;PageRequest&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;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSize&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;getSortOrDefault&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSortBy&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSortDirection&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;And filtering falls back to "match everything" rather than calling the abstract &lt;code&gt;createSpecification(params)&lt;/code&gt; with nothing for it to read:&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;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResultPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toPage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&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="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unrestricted&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;createSpecification&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&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;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&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;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;content&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="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;transformer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;createOutput&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResultPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="na"&gt;getTotalPages&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="na"&gt;getTotalElements&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;content&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;code&gt;Specification.unrestricted()&lt;/code&gt; is doing real work in that ternary, not just avoiding a null check. The first instinct is usually to pass &lt;code&gt;Specification.where(null)&lt;/code&gt; - which is the older, more commonly documented way of expressing "no restriction" in Spring Data JPA. But recent Spring Data versions added a second &lt;code&gt;where(...)&lt;/code&gt; overload for &lt;code&gt;PredicateSpecification&lt;/code&gt;, and a bare &lt;code&gt;null&lt;/code&gt; literal is ambiguous between the two. The compiler can't infer which &lt;code&gt;where&lt;/code&gt; you meant, so it refuses to guess. &lt;code&gt;unrestricted()&lt;/code&gt; exists to sidestep exactly that ambiguity, it says "match everything" without needing a null argument for the compiler to argue about.&lt;/p&gt;

&lt;p&gt;These issues stay hidden if we only test scenarios where valid parameters are provided. Handling unexpected edge cases is where a test suite truly adds value, the &lt;code&gt;testSearchNullParams&lt;/code&gt; test might seem boring because it just calls the &lt;code&gt;search&lt;/code&gt; method with null param, but it proved its worth because it prevented letting a bug to impact other parts of the system.&lt;/p&gt;




&lt;h3&gt;
  
  
  Choosing an In-Memory Database
&lt;/h3&gt;

&lt;p&gt;Component tests need to be fast enough that nobody's tempted to skip them, which rules out spinning up a containerized database for every run of a basic CRUD suite. That leaves a choice between H2 and HSQLDB, and it's worth being honest about what that choice costs.&lt;/p&gt;

&lt;p&gt;We run H2 in strict compatibility mode (&lt;code&gt;MODE=MySQL&lt;/code&gt;, matching our production dialect) for this layer, mainly because startup is sub-second and there's no container to wait on which matters more than it sounds like it should, multiplied across a few hundred local test runs a day. HSQLDB is arguably more stable in isolation, but its dialect compliance with modern MySQL and PostgreSQL features - JSON columns, window functions, strict identifier quoting, lags enough that a test can pass against HSQLDB and break the moment it hits real MySQL. Either way, neither in-memory engine replicates the production query planner, locking behavior, or dialect-specific edge cases. That was never the job we're asking them to do here.&lt;/p&gt;

&lt;p&gt;So we split the responsibility deliberately: H2 handles fast, local validation of the mapping and constraint logic through the DAO proxies in this chapter. Later in the series, when we get to end-to-end integration testing, a dedicated suite runs against a real MySQL 9 Testcontainer to validate the behavior H2 can't, that's the layer that's actually authoritative for production-identical engine behavior, and it's allowed to be slower because it runs less often.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;With the DAO layer's persistence boundaries actually proven against real database round-trips - not just Hibernate's bookkeeping, the natural next question is what happens to boundaries like this one under deadline pressure.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Chapter 8: Why almost every ‘Temporary’ Workaround is Permanent&lt;/strong&gt;, we step back from the happy-path development. The biggest threat to a clean multi-module boundary usually isn’t a missing design pattern, it’s the workaround that was supposed to be temporary. We’ll look at how an unisolated external dependency, a quick infrastructure leak, or a skipped domain boundary compounds quietly over time, before moving on to building the business service layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 7, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-07-dao-testing&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-07-dao-testing" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/the-dao-implementation-layer-chapter-6-4ekg"&gt;Read Chapter 6: The DAO Implementation Layer&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/why-almost-every-temporary-workaround-is-permanent-chapter-8-2on5"&gt;Read Chapter 8: Why almost every ‘Temporary’ Workaround is Permanent&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>java</category>
      <category>spring</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Dao Implementation Layer (Chapter 6)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:52:51 +0000</pubDate>
      <link>https://dev.to/kamenivanov/the-dao-implementation-layer-chapter-6-4ekg</link>
      <guid>https://dev.to/kamenivanov/the-dao-implementation-layer-chapter-6-4ekg</guid>
      <description>&lt;p&gt;The last 11 years through my career, I've seen codebases that suffered from a silent architectural decay. Check any corporate monolith or a microservice and you will find Spring Data repositories, JPA entity annotations, database specifics, and pagination parameters leaking straight into business services, controllers, and domain models. Developers call it "rapid development." What they've actually built is a tightly coupled codebase that is nearly impossible to support, maintain, and bugfix - especially if there are no tests. And guess what? There are none. Because the code is so tightly coupled, developers don't have the time, or honestly the patience, to mock all those dependencies or try to disentangle the mess just to make their lives easier. Divide and conquer - it's the rule I live by whenever I'm building a new service, REST endpoint, or Kafka integration.&lt;/p&gt;

&lt;p&gt;In Chapter 2 and Chapter 3, we drew a hard boundary. We established a 100% pure Java domain model completely free of database metadata, and we defined clean DAO API contracts. In Chapter 4, we've shown why we use direct, JIT-optimized Java transformers instead of reflection-based mapping like MapStruct, and in Chapter 5, we explained why we don't even allow Lombok as a dependency.&lt;/p&gt;

&lt;p&gt;Now, it's time to open up the engine room. We are implementing the &lt;code&gt;dao-impl&lt;/code&gt; infrastructure module using Java 25, Spring Data JPA, and Hibernate. We will look at how generic hierarchy extension and custom Specification execution keep our persistence details locked safely away from the rest of the application.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;CrudDaoImpl&lt;/code&gt; and &lt;code&gt;SearchableDaoImpl&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Writing boilerplate CRUD and search logic for every single database table is a waste of resources and time. At the same time, copy-pasting generic utility code creates maintenance nightmares. We solve this by structuring a clean, type-safe inheritance hierarchy.&lt;/p&gt;

&lt;p&gt;At the base sits &lt;code&gt;CrudDaoImpl&lt;/code&gt;. It handles standard entity creation, lifecycle validation, and ID lookups while enforcing the boundary between domain objects and database entities:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CrudDaoImpl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Entity&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatableEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Repo&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;CrudDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Validator&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Repo&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;protected&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BiTransformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;CrudDaoImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Repo&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;BiTransformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Validator&lt;/span&gt; &lt;span class="n"&gt;validator&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transformer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transformer&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;validator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="n"&gt;domain&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;domain&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Entity is mandatory"&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;domain&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="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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Entity existing!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;preCreate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;validateEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;entity&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;entity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;preCreate&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;entity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCreatedAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&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;entity&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;AbstractUpdatableEntity&lt;/span&gt; &lt;span class="n"&gt;updatableEntity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;updatableEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUpdatedAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&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="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;preUpdate&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;entity&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;entity&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;AbstractUpdatableEntity&lt;/span&gt; &lt;span class="n"&gt;updatableEntity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;updatableEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUpdatedAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&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;// Additional shared CRUD machinery...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;preCreate&lt;/code&gt; and &lt;code&gt;preUpdate&lt;/code&gt; reach for &lt;code&gt;instanceof&lt;/code&gt; instead of calling &lt;code&gt;setUpdatedAt()&lt;/code&gt; directly on &lt;code&gt;Entity&lt;/code&gt;. That's deliberate, not a gap. &lt;code&gt;CrudDaoImpl&lt;/code&gt; has to stay generic over every entity in the hierarchy, including append-only ones like &lt;code&gt;CategoryAssignment&lt;/code&gt; that never extend &lt;code&gt;AbstractUpdatableEntity&lt;/code&gt; at all - so &lt;code&gt;Entity&lt;/code&gt; can only ever be statically bounded by &lt;code&gt;AbstractCreatableEntity&lt;/code&gt;, and the compiler genuinely has no way to know, from inside this class, whether a given &lt;code&gt;Entity&lt;/code&gt; also supports &lt;code&gt;updatedAt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The alternative would be a parallel &lt;code&gt;UpdatableDaoImpl&lt;/code&gt; layer bounded by &lt;code&gt;AbstractUpdatableEntity&lt;/code&gt;, sitting next to &lt;code&gt;SearchableDaoImpl&lt;/code&gt;. That sounds cleaner until you remember Java only allows single inheritance: &lt;code&gt;ProductDaoImpl&lt;/code&gt; already extends &lt;code&gt;SearchableDaoImpl&lt;/code&gt;, so it couldn't also extend a second base class for updatability without either duplicating the search machinery into a combinator class or moving the whole hierarchy to interfaces with default methods. One &lt;code&gt;instanceof&lt;/code&gt; check that anyone can read top to bottom is a smaller price than that.&lt;/p&gt;

&lt;p&gt;It's also worth being precise about why this isn't the reflection-based magic we spent earlier chapters arguing against: &lt;code&gt;instanceof&lt;/code&gt; is a single, explicit, statically-checked type test that the compiler verifies at compile time and the JIT can inline - nothing here is scanning annotations, generating bytecode, or hiding behind a &lt;code&gt;target/generated-sources&lt;/code&gt; folder. Runtime type inspection and reflection-based mapping look similar on the surface, but they aren't the same tool, and conflating them would undercut the whole argument this series has been making.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;preCreate&lt;/code&gt; captures a single &lt;code&gt;now&lt;/code&gt; and reuses it for both fields rather than calling &lt;code&gt;Instant.now()&lt;/code&gt; twice, so &lt;code&gt;createdAt == updatedAt&lt;/code&gt; on a fresh insert is guaranteed exactly, not just approximately - which is what actually makes that equality useful as a "never touched since creation" signal.&lt;/p&gt;




&lt;h4&gt;
  
  
  Why timestamps are set here, not left to the database
&lt;/h4&gt;

&lt;p&gt;The Flyway migration for this table will have a &lt;code&gt;DEFAULT CURRENT_TIMESTAMP&lt;/code&gt; on the &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;updated_at&lt;/code&gt; columns, so it's worth asking directly: if the database is already going to fill it in, why does &lt;code&gt;preCreate&lt;/code&gt; and &lt;code&gt;preUpdate&lt;/code&gt; set it again in application code? Because the database default and the application value are solving two different problems.&lt;/p&gt;

&lt;p&gt;The DB default is a safety net. It exists for anything that touches the table outside the application: a manual &lt;code&gt;INSERT&lt;/code&gt; during an incident, a backfill script, another service hitting the same schema directly. It guarantees the column is never null, no matter what wrote the row.&lt;/p&gt;

&lt;p&gt;The application-level value is the source of truth for anything going through this DAO, and setting it explicitly in &lt;code&gt;preCreate&lt;/code&gt; matters for a concrete reason: Hibernate doesn't automatically read back database-computed defaults after &lt;code&gt;save()&lt;/code&gt;. Relying on the DB default alone means the in-memory entity still has &lt;code&gt;createdAt == null&lt;/code&gt; right after the insert, and &lt;code&gt;create()&lt;/code&gt; hands that same object straight to &lt;code&gt;transformer.createOutput(entity)&lt;/code&gt; - so the caller gets back a &lt;code&gt;Product&lt;/code&gt; with a null creation timestamp for an entity that very much has one in the database. That's a real bug, not a style nitpick.&lt;/p&gt;

&lt;p&gt;The same reasoning is why &lt;code&gt;preCreate&lt;/code&gt; sets &lt;code&gt;updatedAt&lt;/code&gt; too, not just &lt;code&gt;createdAt&lt;/code&gt;. &lt;code&gt;updated_at&lt;/code&gt; is &lt;code&gt;NOT NULL&lt;/code&gt;, same as &lt;code&gt;created_at&lt;/code&gt;, so it can't be left unset until the first real update without hitting the exact null-after-save problem above - except this time the DB default that would normally paper over it isn't guaranteed to fire at all, since &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt; defaults are typically written to apply on insert, not on every column independently. Setting both fields from a single captured &lt;code&gt;now&lt;/code&gt; sidesteps that entirely, and gets you &lt;code&gt;createdAt == updatedAt&lt;/code&gt; as a free signal that a record has never been touched since creation, with no extra flag to maintain.&lt;/p&gt;




&lt;h4&gt;
  
  
  Immutable Audit Fields on the Domain Side
&lt;/h4&gt;

&lt;p&gt;The DAO layer owning &lt;code&gt;createdAt&lt;/code&gt;/&lt;code&gt;updatedAt&lt;/code&gt; only works if the domain side agrees to stay out of the way. Earlier versions of our domain classes had default no-arg constructors and public setters, which left audit fields mutable and gave the business layer a path to overwrite them by accident.&lt;/p&gt;

&lt;p&gt;We removed both. Domain classes no longer have no-arg constructors, and &lt;code&gt;createdAt&lt;/code&gt;/&lt;code&gt;updatedAt&lt;/code&gt; are &lt;code&gt;final&lt;/code&gt; - set once, via the reconstitution constructors covered in Chapter 2, and never touched again for the life of that object. Our transformers reflect this split cleanly: writing domain-to-entity, &lt;code&gt;createInput()&lt;/code&gt; ignores audit fields entirely, since &lt;code&gt;preCreate&lt;/code&gt;/&lt;code&gt;preUpdate&lt;/code&gt; above already own them on the entity side; reading entity-to-domain, &lt;code&gt;createOutput()&lt;/code&gt; is the only caller that ever passes a real &lt;code&gt;createdAt&lt;/code&gt;/&lt;code&gt;updatedAt&lt;/code&gt; into a domain constructor, because it's the only place that has the real, already-persisted values to pass. No service layer and no client payload has a path to either field.&lt;/p&gt;

&lt;p&gt;When a domain requires complex search capabilities, pagination, and sorting, we extend &lt;code&gt;CrudDaoImpl&lt;/code&gt; with &lt;code&gt;SearchableDaoImpl&lt;/code&gt;. Instead of letting business services deal with JPA Criteria APIs or Spring Data Specification objects, the searchable DAO encapsulates query execution internally:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchableDaoImpl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Domain&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Entity&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatableEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Repo&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractParams&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SortBy&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudDaoImpl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;,&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;Repo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;SearchableDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt; &lt;span class="no"&gt;ID_DESC_SORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DESC&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt; &lt;span class="no"&gt;CREATED_AT_DESC_SORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"createdAt"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;descending&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ID_DESC_SORT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;SearchableDaoImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Repo&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;BiTransformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Validator&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&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;transformer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResultPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Pageable&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toPage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createSpecification&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&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;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&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;Domain&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;content&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="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;transformer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;createOutput&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResultPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="na"&gt;getTotalPages&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="na"&gt;getTotalElements&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createSpecification&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Params&lt;/span&gt; &lt;span class="n"&gt;params&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;h4&gt;
  
  
  Why &lt;code&gt;Repo&lt;/code&gt; needs two bounds, not one
&lt;/h4&gt;

&lt;p&gt;Look closely at the type bound on &lt;code&gt;SearchableDaoImpl&lt;/code&gt;:&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="nc"&gt;Repo&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;&amp;amp;&lt;/code&gt; is an intersection type, and it's doing more work than it looks like. &lt;code&gt;CrudRepository&lt;/code&gt; gives us &lt;code&gt;save()&lt;/code&gt;, &lt;code&gt;findById()&lt;/code&gt;, &lt;code&gt;delete()&lt;/code&gt; - the basic CRUD operations that &lt;code&gt;CrudDaoImpl&lt;/code&gt; needs. But &lt;code&gt;CrudRepository&lt;/code&gt; has no idea what a &lt;code&gt;Specification&lt;/code&gt; is, that interface lives entirely in &lt;code&gt;JpaSpecificationExecutor&lt;/code&gt;, which is what actually declares &lt;code&gt;findAll(Specification&amp;lt;T&amp;gt;, Pageable)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we bounded &lt;code&gt;Repo&lt;/code&gt; on &lt;code&gt;CrudRepository&lt;/code&gt; alone, the &lt;code&gt;search()&lt;/code&gt; method above simply wouldn't compile - &lt;code&gt;repository.findAll(spec, pageable)&lt;/code&gt; calls a method that doesn't exist on that interface. If we bounded it on &lt;code&gt;JpaSpecificationExecutor&lt;/code&gt; alone, we'd lose &lt;code&gt;save()&lt;/code&gt; and the rest of the CRUD surface that &lt;code&gt;CrudDaoImpl&lt;/code&gt; depends on. We need both interfaces satisfied by the same concrete repository type, and Java's intersection bounds are the only clean way to express "this generic parameter must implement A and B" without collapsing the two responsibilities into one bloated interface.&lt;/p&gt;

&lt;p&gt;The payoff shows up downstream, in &lt;code&gt;ProductRepository&lt;/code&gt; and &lt;code&gt;CategoryRepository&lt;/code&gt;, they simply extend both interfaces, and every method from both becomes available on &lt;code&gt;repository&lt;/code&gt; inside the DAO, fully typed, with no casting. If any entity doesn't need to be paginated, it will simply extend only the &lt;code&gt;CrudRepository&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice also how sorting is handled above. By appending &lt;code&gt;id DESC&lt;/code&gt; as a deterministic tie-breaker to every sort order, we eliminate unstable pagination results when multiple records share identical timestamps or property values. I've chased that exact bug in production before - page 3 of a listing quietly returns a row you already saw on page 2, because two rows had the same &lt;code&gt;createdAt&lt;/code&gt; millisecond and the database made no promises about their relative order.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;ProductDaoImpl&lt;/code&gt; and &lt;code&gt;CategoryDaoImpl&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;With the generic abstraction we just created, implementing concrete data access objects requires minimal code. Look at how clean &lt;code&gt;CategoryDaoImpl&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CategoryDaoImpl&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SearchableDaoImpl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Category&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CategoryEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CategoryRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CategorySearchParams&lt;/span&gt;
        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;CategoryDao&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;CategoryDaoImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategoryRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Validator&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&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="nc"&gt;CategoryTransformer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CategoryEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createSpecification&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CategorySearchParams&lt;/span&gt; &lt;span class="n"&gt;params&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="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;final&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;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;predicates&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;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;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;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;predicates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;like&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lower&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"%"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"%"&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;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getActive&lt;/span&gt;&lt;span class="o"&gt;()&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="n"&gt;predicates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"active"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getActive&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="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toArray&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;Predicate&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="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;For domain-specific queries like SKU lookups, &lt;code&gt;ProductDaoImpl&lt;/code&gt; delegates directly to custom repository methods while keeping the transformation logic fully 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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductDaoImpl&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SearchableDaoImpl&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ProductEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ProductRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ProductSearchParams&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ProductDao&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;ProductDaoImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Validator&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&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="nc"&gt;ProductTransformer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nf"&gt;loadBySku&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;sku&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&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;loadBySku&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createSpecification&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductSearchParams&lt;/span&gt; &lt;span class="n"&gt;params&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="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;final&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;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;predicates&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;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;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;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;()){&lt;/span&gt;
                &lt;span class="n"&gt;predicates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;like&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lower&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"%"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"%"&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="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toArray&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;Predicate&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="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;h4&gt;
  
  
  Why I don't use Spring Data Method Naming
&lt;/h4&gt;

&lt;p&gt;Look at &lt;code&gt;ProductRepository&lt;/code&gt;, or any custom repository method in this codebase, and you won't find something like &lt;code&gt;findByProductStatusAndNameContainingIgnoreCaseAndCreatedAtAfterOrderByPriceDesc&lt;/code&gt;. Spring Data will happily generate that query for you from the method name alone - no annotation, no SQL, just string parsing at startup. It seems easy and intuitive, but when we get in a production service taking real traffic, I don't want it anywhere near my persistence layer, for a few concrete reasons.&lt;/p&gt;

&lt;p&gt;The first is fragility, if we rename a field on the entity, let's say for example &lt;code&gt;sku&lt;/code&gt; becomes &lt;code&gt;productSku&lt;/code&gt;, and the derived-name query built on the old name either breaks at startup with a &lt;code&gt;PropertyReferenceException&lt;/code&gt;, or, depending on how the method's structured, doesn't break at all and just silently stops matching what you think it matches. Either way, the compiler never told you. An explicit query, by contrast, references the actual JPQL or SQL, so a rename is a normal refactor with a normal compile error, not a runtime surprise waiting for the right request to trigger it.&lt;/p&gt;

&lt;p&gt;The second is readability, and the method name above is the proof, once a query needs more than two or three conditions, the method name stops being a name and becomes a run-on sentence encoding your entire &lt;code&gt;WHERE&lt;/code&gt; clause in camelCase. Nobody reads that and understands the query faster than they'd understand the five lines of JPQL it's standing in for - they read it slower, and they have to mentally decode &lt;code&gt;ContainingIgnoreCase&lt;/code&gt; and &lt;code&gt;OrderByPriceDesc&lt;/code&gt; back into SQL concepts they already know.&lt;/p&gt;

&lt;p&gt;The third is the one that actually matters most at midnight during an incident: derived queries hide the generated SQL from you until it runs. An explicit &lt;code&gt;@Query&lt;/code&gt; shows you the exact projection, the exact joins, the exact fetch strategy, right there in the repository interface, during code review, before it had the chance to quietly become an N+1 problem in production.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ProductRepository&lt;/code&gt;'s &lt;code&gt;loadBySku&lt;/code&gt; makes this concrete:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ProductRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CrudRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="nc"&gt;JpaSpecificationExecutor&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT p FROM ProductEntity p WHERE p.sku = :sku"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;ProductEntity&lt;/span&gt; &lt;span class="nf"&gt;loadBySku&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sku"&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;sku&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;Spring Data could derive this one from a method named &lt;code&gt;findBySku&lt;/code&gt; without any annotation at all, it's simple enough. I write the &lt;code&gt;@Query&lt;/code&gt; anyway, because the moment this method needs a join or a second condition, I want it to already look like every other query in this codebase, not like a special case that started simple and grew a name nobody can parse at a glance.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;We've built a flexible persistence infrastructure: clean generic DAO hierarchies, explicit Java transformers, and validation that fails fast in application code instead of at the database.&lt;/p&gt;

&lt;p&gt;Spoiler alert: a green Spring integration test does not necessarily prove that an entity can be written and hydrated correctly by the database. In Chapter 7, we expose Hibernate’s write-behind cache and dirty checking. When a test’s purpose is to verify database constraints or fresh hydration, it should intentionally use &lt;code&gt;.flush()&lt;/code&gt; and &lt;code&gt;.clear()&lt;/code&gt; at the relevant boundary rather than accidentally read managed state from the persistence context.&lt;/p&gt;

&lt;p&gt;See you in the next chapter.&lt;/p&gt;




&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 6, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-06-dao-impl&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-06-dao-impl" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/the-lombok-illusion-chapter-5-56n"&gt;Read Chapter 5: The Lombok Illusion&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/what-spring-data-tests-can-miss-testing-beyond-hibernates-persistence-context-chapter-7-5d7k"&gt;Read Chapter 7: The Persistence layer Testing&lt;/a&gt; &lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>hibernate</category>
    </item>
    <item>
      <title>The Lombok Illusion (Chapter 5)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 10 Aug 2026 06:36:32 +0000</pubDate>
      <link>https://dev.to/kamenivanov/the-lombok-illusion-chapter-5-56n</link>
      <guid>https://dev.to/kamenivanov/the-lombok-illusion-chapter-5-56n</guid>
      <description>&lt;p&gt;You open a new Spring Boot project and you create a DTO, then an entity, and you’re staring at getters, setters, constructors, &lt;code&gt;equals()&lt;/code&gt;, &lt;code&gt;hashCode()&lt;/code&gt;, &lt;code&gt;toString()&lt;/code&gt;. Someone on the team suggests to put lombok’s &lt;code&gt;@Data&lt;/code&gt;, or “just slap &lt;code&gt;@Builder&lt;/code&gt; on it, it’ll be cleaner.” Forty lines become five and it looks great in the PR.&lt;/p&gt;

&lt;p&gt;Then it hits a real codebase, OpenAPI generation doesn't behave the way the build expects. Hibernate meets an auto-generated &lt;code&gt;equals()&lt;/code&gt; and gets confused about identity. Something throws through a generated builder hierarchy at 2 AM, and the method you need to inspect doesn't exist in any file you can open.&lt;/p&gt;

&lt;p&gt;Eleven years into enterprise Java, my rule is simple: Lombok doesn't touch core application behavior. Not because writing a getter is interesting - it isn't, but because the handful of lines it saves rarely covers the compiler magic, tooling friction, and debugging problems it adds to something that has to survive for years after you've moved on to another project.&lt;/p&gt;




&lt;h4&gt;
  
  
  "It just removes boilerplate"
&lt;/h4&gt;

&lt;p&gt;Worth asking what's actually being removed, though. A getter is part of your public API. A setter is a mutation point someone decided to expose. A constructor defines what states an object is allowed to enter. &lt;code&gt;equals()&lt;/code&gt; and &lt;code&gt;hashCode()&lt;/code&gt; define identity. &lt;code&gt;toString()&lt;/code&gt; is what shows up in your logs when things go wrong at 3 AM.&lt;/p&gt;

&lt;p&gt;Write those by hand and they live in the source - visible, searchable, debuggable, owned by whoever's reading the file. Generate them with Lombok and the behavior is still there, it's just moved somewhere you can't see it without a separate step.&lt;/p&gt;

&lt;p&gt;The annotation most people reach for first time is &lt;code&gt;@Data&lt;/code&gt;:&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;@Data&lt;/span&gt;
&lt;span class="nd"&gt;@Entity&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;CustomerEntity&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@OneToMany&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mappedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"customer"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&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;OrderEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&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;One line and you get getters, setters, &lt;code&gt;toString()&lt;/code&gt;, &lt;code&gt;equals()&lt;/code&gt;, &lt;code&gt;hashCode()&lt;/code&gt; across every field. For a JPA entity that's already a problem before you've written any business logic - an entity carries persistence identity, lazy proxies, mutable state, none of which behaves like a plain data bag. Field-based equality ignores identity semantics entirely. A &lt;code&gt;toString()&lt;/code&gt; that walks relationships is a logging hazard waiting for the wrong moment. Public setters on every field quietly throw out encapsulation.&lt;/p&gt;

&lt;p&gt;I've had this exact line take down a support investigation:&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="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Could not process customer: {}"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;toString()&lt;/code&gt; walks a lazy relationship, that log statement fires a database call. If both sides of a bidirectional association reference each other, you get recursion in your logs - I've seen this crash a logging pipeline, not just produce ugly output. And if the persistence context is already closed by the time you log, you can get a &lt;code&gt;LazyInitializationException&lt;/code&gt; thrown while you're trying to log an unrelated failure. A ten-minute incident turns into two hours because the stack trace points at logging code, not the actual bug.&lt;/p&gt;

&lt;p&gt;The usual patch is a pile of exclusions bolted back on:&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;@ToString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exclude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@EqualsAndHashCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onlyExplicitlyIncluded&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At which point we've added a code generator to save typing, then added more annotations to undo part of what it generated, and now every future developer has to understand the interaction between Lombok, Hibernate proxies, equality semantics, and logging before they can touch this class safely. That's not less work. It's the same work, deferred and hidden.&lt;/p&gt;




&lt;h4&gt;
  
  
  Builders aren't free either
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;@Builder&lt;/code&gt; is probably the most defended Lombok annotation, and I get the appeal - long constructors are ugly, named construction reads well at the call site. But a nice call site doesn't automatically mean good architecture underneath it.&lt;/p&gt;

&lt;p&gt;For immutable request models, a record usually does the job better:&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="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;CreateCustomerRequest&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;firstName&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;lastName&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;email&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;No annotation processor, no generated builder class sitting somewhere you'll never look, nothing ambiguous about what exists at compile time - constructor, accessors, &lt;code&gt;equals()&lt;/code&gt;, &lt;code&gt;hashCode()&lt;/code&gt;, &lt;code&gt;toString()&lt;/code&gt;, all from the language itself. Where a builder genuinely earns its place - lots of optional parameters, a real construction protocol - write it by hand. It's not that much typing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@SuperBuilder&lt;/code&gt; I'd flag as the one to be most careful with. Inheritance already makes domain models harder to follow. Add generated nested builders and field shadowing on top of ORM concerns, and you get bugs that compile fine and fail much later. A field ends up null nobody set on purpose, an update silently drops a value, and the row in the database stays technically valid while the business state underneath it is quietly wrong. I've debugged exactly this on a service and it took longer than it should have to even locate which builder was involved.&lt;/p&gt;




&lt;h4&gt;
  
  
  The toolchain cost nobody budgets for
&lt;/h4&gt;

&lt;p&gt;Lombok isn't standard Java. It works by rewriting the compiler's view of your source during annotation processing. That matters because your compiler isn't the only thing reading that code. IDE indexing, static analysis, coverage tools, Javadoc, OpenAPI generation, MapStruct, QueryDSL, whatever other annotation processors are in the build, CI running on a different JDK than your laptop.&lt;/p&gt;

&lt;p&gt;Most of the time it works fine, and that's the trap. It's not that Lombok breaks on day one, it's that it adds one more thing every tool in the pipeline has to keep agreeing on correctly. Springdoc and MapStruct can need specific processor ordering. Lombok's own extensions, like chained accessors, can quietly break JavaBean conventions that some framework tooling assumes without checking. The build accumulates workarounds to compensate, and eventually someone bumps a JDK or an IDE version, and a developer burns half a day figuring out why the generated code and the compiler disagree about what a class looks like.&lt;/p&gt;




&lt;h4&gt;
  
  
  Debugging wants source code, not a description of generated behavior
&lt;/h4&gt;

&lt;p&gt;When something breaks in production I want a straight line: stack trace, source line, method body, state change, fix. Generated code adds fog to every step of that. The file in front of you tells you an annotation exists, not what it expands to. Builders become generated nested types you've never opened. Generated &lt;code&gt;equals()&lt;/code&gt; and &lt;code&gt;toString()&lt;/code&gt; pull behavior into an incident that nobody in the room actually wrote.&lt;/p&gt;

&lt;p&gt;You can delombok a class to see what it really compiles to, but needing a second, generated copy of your own source just to understand what the first one does isn't a workaround. It's the actual problem, restated.&lt;/p&gt;




&lt;h4&gt;
  
  
  Java 25 already solves this
&lt;/h4&gt;

&lt;p&gt;Lombok got popular in an older Java - verbose syntax, no records, weaker IDE support. We're not in that world anymore, the language itself covers most of what people reach for Lombok to fix:&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="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;CustomerResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&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;fullName&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;email&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerEntity&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&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;CustomerEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&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;email&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="nf"&gt;getId&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="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&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;String&lt;/span&gt; &lt;span class="nf"&gt;getEmail&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="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&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;changeEmail&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;email&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&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;code&gt;changeEmail()&lt;/code&gt; tells you more than a generated &lt;code&gt;setEmail()&lt;/code&gt; ever would - it names intent, and naming intent is domain modeling, not boilerplate. For the rare getter or constructor that really is trivial, let the IDE generate it once. That costs less time than a Maven exclusion you'll be maintaining six months from now.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where I've landed
&lt;/h4&gt;

&lt;p&gt;Records for immutable DTOs like requests, responses, query results. Explicit classes for JPA entities and anything with mutable infrastructure state. Constructors written by hand to protect valid state. Explicit equality on entities where identity actually matters, not field-by-field comparison. No blanket &lt;code&gt;@Data&lt;/code&gt;, and nothing generated that the next engineer has to reverse-engineer just to trust the class.&lt;/p&gt;

&lt;p&gt;A dependency that shrinks your code but makes the build, the debugger, and the IDE more fragile isn't really improving developer experience, it's borrowing against it, and the interest comes due later. Small at first, then the module count grows, tooling gets stricter, JDK versions move on, and someone's maintaining a pile of invisible generated behavior that nobody currently on the team signed up to own.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;Stripping out the bytecode magic keeps the codebase predictable, debuggable, and aligned with the standard toolchain instead of fighting it.&lt;/p&gt;

&lt;p&gt;Next up, &lt;strong&gt;Chapter 6: Implementing the DAO Layer&lt;/strong&gt; - wiring up Spring Data JPA and Hibernate behind clean data access interfaces, with explicit, reflection-free transformation strategies that respect the database boundary and keep the domain layer pristine.&lt;/p&gt;




&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 5, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-05-lombok&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-05-lombok" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/mapping-strategies-without-the-magic-chapter-4-25e4"&gt;Read Chapter 4: Mapping Strategies Without the Magic&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/the-dao-implementation-layer-chapter-6-4ekg"&gt;Read Chapter 6: The Dao Implementation Layer&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>java</category>
      <category>softwareengineering</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Mapping Strategies Without the Magic (Chapter 4)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:22:28 +0000</pubDate>
      <link>https://dev.to/kamenivanov/mapping-strategies-without-the-magic-chapter-4-25e4</link>
      <guid>https://dev.to/kamenivanov/mapping-strategies-without-the-magic-chapter-4-25e4</guid>
      <description>&lt;p&gt;At the end of the previous chapter, I teased that we were about to dive straight into the heavy machinery of DAO implementation - hooking up Spring Data JPA and Hibernate under the hood.&lt;/p&gt;

&lt;p&gt;If you look at our original roadmap, concrete implementation was supposed to be right here. But after laying down our domain models, data contracts, and reading some of your feedback, I realized that we need to address an unspoken architectural trap first: &lt;strong&gt;data mapping and transformation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most teams blindly adopt automated tools - whether runtime reflection wrappers or compile-time generators like MapStruct - until an architectural mismatch or production incident breaks their service. Before we wire up our database infrastructure, let’s see why we chose to completely bypass mapping "magic" in favor of pure, explicit Java transformers.&lt;/p&gt;

&lt;p&gt;And yes, some will say that writing explicit transformers is boilerplate - why write it manually when we can just use an annotation? The answer is simple: we’re not building a simple CRUD application that gets thrown to a support team and forgotten. We’re building an enterprise-ready microservice, built for deployment in Kubernetes, integrated with Kafka, Redis, and multi-tenant authorization layers - a product designed to be actively developed and maintained over years, not weeks.&lt;/p&gt;

&lt;p&gt;This is where the real value shines: spending a little more time writing explicit &lt;strong&gt;transformers&lt;/strong&gt; - as I call them, rather than standard Mappers. I call them transformers because they actively reshape data. A "mapping" usually implies just copying a field from &lt;code&gt;ClassA&lt;/code&gt; to &lt;code&gt;ClassB&lt;/code&gt; (whether with the same or a different name). We aren't doing that here because at an enterprise level, field names, types, and structural representations will diverge significantly between your database entities, domain models, and external DTOs.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Architectural Trap
&lt;/h3&gt;

&lt;p&gt;Across my 11 years in Enterprise Java, I've seen team after team reach for automated mappers to "save time." To understand why we banned them, we must separate them into two distinct categories - because they fail for entirely different technical reasons.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The Reflection Black Box (ModelMapper, Dozer, Spring BeanUtils)
&lt;/h4&gt;

&lt;p&gt;Runtime reflection tools attempt to inspect fields dynamically during application execution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Production Tax:&lt;/strong&gt; They rely on deep reflection APIs, bypassing Java's strong compile-time type safety.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Debugging Wall:&lt;/strong&gt; When a field type changes or a property is missing, the application crashes under load with a 50-line stack trace originating from the library's internal reflection routines—giving you zero visibility into which object or field caused the failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Drag:&lt;/strong&gt; Continuously resolving fields via reflection adds memory allocation and CPU overhead to every single request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. The Compile-Time Generation (MapStruct)
&lt;/h4&gt;

&lt;p&gt;Unlike reflection tools, MapStruct generates standard Java code in &lt;code&gt;target/generated-sources/&lt;/code&gt; during compilation. It is inspectable, debuggable, and fast at runtime.&lt;/p&gt;

&lt;p&gt;To be fair: for a simple, flat 1:1 DTO with no structural divergence, MapStruct works fine and gets out of your way. But the pain scales linearly with how much your layers diverge - and in real-world enterprise systems, domain aggregates, database entities, and REST DTOs diverge rapidly.&lt;/p&gt;

&lt;p&gt;When structural divergence happens, MapStruct introduces distinct friction points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IDE Refactoring Friction:&lt;/strong&gt; MapStruct relies on string-based path configurations: &lt;code&gt;@Mapping(source = "shippingDetails.address.street", target = "streetAddress")&lt;/code&gt;. IDE rename refactoring doesn't reach into these annotation strings the way it does for real Java field references. Even with dedicated MapStruct IDE plugins installed, you're relying on static inspection warnings after the fact, rather than a guaranteed, seamless edit-time rewrite across your codebase.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fallacy of 1:1 Matching:&lt;/strong&gt; If your &lt;em&gt;Domain Model&lt;/em&gt; (pure POJO/Record protecting business invariants), your &lt;em&gt;JPA Entity&lt;/em&gt; (polluted by Hibernate requirements, proxies, and persistence states), and your &lt;em&gt;REST DTO&lt;/em&gt; (flat JSON contract) look identical and map 1:1... you have a severe architectural issue. They &lt;strong&gt;should never&lt;/strong&gt; match 1:1. The domain protects business rules. The entity fights the relational database. The DTO adapts to external API clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework Leakage into Domain Logic:&lt;/strong&gt; The moment your domain uses composite hierarchies or custom value objects, MapStruct forces you to write custom &lt;code&gt;@Named&lt;/code&gt; helper methods or embed raw Java strings inside annotation parameters: &lt;code&gt;expression = "java(new Money(...))"&lt;/code&gt;. Writing raw Java code inside annotation strings is a clear indicator that the abstraction has broken down.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Composite Hierarchy Reality Check: MapStruct vs. Pure Java
&lt;/h3&gt;

&lt;p&gt;MapStruct tutorials always feature trivial 1:1 flat classes (&lt;code&gt;UserDto&lt;/code&gt; to &lt;code&gt;UserEntity&lt;/code&gt;). But real enterprise software uses &lt;strong&gt;rich composite value objects&lt;/strong&gt; on the domain side while entities flatten them or restructure audit information. Let’s compare how MapStruct handles composite hierarchies versus how pure Java solves it safely.&lt;/p&gt;

&lt;h4&gt;
  
  
  The MapStruct Way (Annotation Spaghetti &amp;amp; Untyped String Expressions)
&lt;/h4&gt;

&lt;p&gt;When field names mismatch, unit conversions are required, or composite objects need instantiation, MapStruct forces you into string-based paths and Java string expressions:&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;@Mapper&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;componentModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"spring"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;OrderMapper&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"totalMoney.amount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"totalAmountCentimes"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qualifiedByName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"amountToCentimes"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"totalMoney.currency"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"currencyCode"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shippingDetails.recipientName"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"deliveryContact"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shippingDetails.address.street"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"streetAddress"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shippingDetails.address.zipCode"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"postalCode"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;OrderEntity&lt;/span&gt; &lt;span class="nf"&gt;toEntity&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;domain&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@Named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amountToCentimes"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;amountToCentimes&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;amount&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;amount&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0L&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="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&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="na"&gt;longValue&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@InheritInverseConfiguration&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"totalMoney"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;expression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"java(new Money(entity.getTotalAmountCentimes(), entity.getCurrencyCode()))"&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;toDomain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEntity&lt;/span&gt; &lt;span class="n"&gt;entity&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;Notice what happened: to handle a simple composite conversion, we ended up writing untyped Java inside an annotation string parameter (&lt;code&gt;expression = "java(...)"&lt;/code&gt;) and configuring string property paths that don't refactor cleanly with standard IDE tools.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Pure Java Transformer Way (Zero Reflection, 100% Control)
&lt;/h4&gt;

&lt;p&gt;In our multi-module architecture, we stripped out both runtime reflection libraries and compile-time code generators. We replaced them with simple, fully deterministic contracts written in vanilla Java. Here are the root interfaces that dictate the contracts for data transformation:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Transformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  

    &lt;span class="cm"&gt;/**
     * Creates a new instance of an Output type, with transformed data from the input object.
     * @param input the input object  
     * @return the newly created Output object  
     */&lt;/span&gt;    
    &lt;span class="nc"&gt;Output&lt;/span&gt; &lt;span class="nf"&gt;createOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  

    &lt;span class="cm"&gt;/**
     * Copies the data from input to output.     
     * @param input  the input object  
     * @param output the output object  
     */&lt;/span&gt;    
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;copyToOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Output&lt;/span&gt; &lt;span class="n"&gt;output&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And our transformer contract for reverse transformations (used when models require bi-directional mapping across boundaries):&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BiTransformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Transformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  

    &lt;span class="cm"&gt;/**
     * Creates a new instance of an Input type, with transformed data from the output object. 
     * @param output the output object  
     * @return the newly created Input object  
     */&lt;/span&gt;
    &lt;span class="nc"&gt;Input&lt;/span&gt; &lt;span class="nf"&gt;createInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Output&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  

    &lt;span class="cm"&gt;/**
     * Copies the data from output to input. 
     * @param output the output object 
     * @param input  the input object  
     */&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;copyToInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Output&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Input&lt;/span&gt; &lt;span class="n"&gt;input&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  Leveraging Hierarchy: Polymorphic Auditing Without Annotation Duplication
&lt;/h4&gt;

&lt;p&gt;To see how this works beyond abstract interfaces, look at our foundational audit layer. Just as we have domain-level base models, our infrastructure layer features shared concepts like creation and auditing (&lt;code&gt;AbstractCreatableEntity&lt;/code&gt; in &lt;code&gt;dao-impl&lt;/code&gt; and &lt;code&gt;AbstractCreatable&amp;lt;UUID&amp;gt;&lt;/code&gt; in &lt;code&gt;domain&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Instead of writing repetitive &lt;code&gt;@Mapping&lt;/code&gt; annotations across dozens of child mappers, our transformer hierarchy follows the class hierarchy:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatableTransformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="no"&gt;S&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatableEntity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractBiTransformer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;S&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&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;copyToInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;S&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&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="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCreatedAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCreatedAt&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&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;copyToOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;S&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCreatedAt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCreatedAt&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;Every concrete implementation extends &lt;code&gt;AbstractCreatableTransformer&lt;/code&gt; and gains out-of-the-box, strongly-typed transformation of parent audit fields without a single line of duplicated configuration or annotation processing.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Honest Trade-Off: What We Give Up
&lt;/h3&gt;

&lt;p&gt;I don't claim pure Java transformers come at zero cost. There is no free lunch in software architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More Physical Files:&lt;/strong&gt; You write and maintain explicit &lt;code&gt;.java&lt;/code&gt; transformer classes for every boundary mapping rather than single-line interface annotations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial Writing Time:&lt;/strong&gt; Setting up a direct transformer takes a minute longer than dropping an &lt;code&gt;@Mapper&lt;/code&gt; annotation on a brand-new entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding Learning Curve:&lt;/strong&gt; New hires accustomed to MapStruct or ModelMapper have to learn your module hierarchy (&lt;code&gt;Transformer&lt;/code&gt; vs. &lt;code&gt;BiTransformer&lt;/code&gt;) before they write their first transformer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We deliberately pay this price. We trade a few seconds of initial writing time for 100% compile-time predictability, zero annotation processor build friction, seamless IDE refactoring, and instant local debugging under pressure.&lt;/p&gt;




&lt;h3&gt;
  
  
  What we win for the price we pay
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0% Reflection Magic:&lt;/strong&gt; The compiler knows everything. Every assignment is an explicit, strongly-typed method call (&lt;code&gt;get&lt;/code&gt; / &lt;code&gt;set&lt;/code&gt; / constructor / builder).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guaranteed Refactoring Safety:&lt;/strong&gt; Rename any field or record component in your domain model, and your IDE renames it across all transformer classes natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Java Strings in Annotations:&lt;/strong&gt; You write real Java code in Java files—not inside &lt;code&gt;expression = "java(...)"&lt;/code&gt; annotation parameter strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JIT-Optimized Direct Assignments:&lt;/strong&gt; The JVM easily unrolls and inlines straightforward Java method calls directly into native machine code. There is simply no faster execution path than raw Java.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Debugging Clarity:&lt;/strong&gt; Place a breakpoint anywhere inside your transformer, hit F7, and inspect your real in-memory objects step-by-step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Annotation Processor Conflicts:&lt;/strong&gt; No extra build-phase steps, no processor ordering headaches between tools, and zero build plugin friction.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;Before we open up the &lt;code&gt;dao-impl&lt;/code&gt; module and start building concrete JPA implementations, we have one more compile-time trap to tear down. In &lt;strong&gt;Chapter 5: The Lombok Illusion&lt;/strong&gt;, we will see why we removed Lombok from our builds and how modern Java features make AST bytecode mutation obsolete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 4, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-04-mappers&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-04-mappers" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/anatomy-of-the-data-access-contract-protect-your-domain-from-framework-leaks-chapter-3-ni"&gt;Read Chapter 3: Anatomy of the Data Access Contract&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/the-lombok-illusion-chapter-5-56n"&gt;Read Chapter 5: The Lombok Illusion&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Anatomy of the Data Access Contract: Protect Your Domain from Framework Leaks (Chapter 3)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Mon, 27 Jul 2026 07:27:55 +0000</pubDate>
      <link>https://dev.to/kamenivanov/anatomy-of-the-data-access-contract-protect-your-domain-from-framework-leaks-chapter-3-ni</link>
      <guid>https://dev.to/kamenivanov/anatomy-of-the-data-access-contract-protect-your-domain-from-framework-leaks-chapter-3-ni</guid>
      <description>&lt;p&gt;In almost every modern Java textbook, data persistence is introduced with a dangerous shortcut. You are told to create an interface, extend &lt;code&gt;JpaRepository&amp;lt;Product, UUID&amp;gt;&lt;/code&gt;, and call it a day.&lt;/p&gt;

&lt;p&gt;The result? Without even noticing, you have tightly coupled your entire application to Spring Data and Hibernate.&lt;/p&gt;

&lt;p&gt;The moment a junior developer leaks &lt;code&gt;Pageable&lt;/code&gt; or &lt;code&gt;Specification&lt;/code&gt; from Spring into your clean &lt;code&gt;@Service&lt;/code&gt; layer, or worse—directly into your REST controllers (as we discussed in Chapter 1)—your clean architecture is officially dead. Your core module now depends on the database infrastructure framework.&lt;/p&gt;

&lt;p&gt;If tomorrow you want to switch a high-throughput read-heavy path to raw JDBC, or migrate a specific sub-domain to MongoDB, you will realize your business logic is trapped in a framework prison.&lt;/p&gt;

&lt;p&gt;In Chapter 3 of our Evolutionary Architecture series, we are building the &lt;strong&gt;Dao API Module&lt;/strong&gt;. A zero-dependency, pure Java module that strictly defines the contracts for persistence and querying, without knowing a single thing about SQL, Hibernate, or Spring.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Separation of Concerns: Split Creation from Updates
&lt;/h3&gt;

&lt;p&gt;A common mistake in generic DAO design is introducing a single &lt;code&gt;save(Entity)&lt;/code&gt; method that acts as both an &lt;code&gt;INSERT&lt;/code&gt; and an &lt;code&gt;UPDATE&lt;/code&gt; (the typical CRUD/Upsert pattern).&lt;/p&gt;

&lt;p&gt;While frameworks love this because it hides complexity, it introduces an architectural and performance issue.&lt;/p&gt;

&lt;p&gt;When creating a new domain object, it enters the system without an ID, allowing the infrastructure layer (like Hibernate with its modern, time-ordered &lt;strong&gt;UUIDv7 generator&lt;/strong&gt;) to assign it upon persistence. However, when updating an existing entity, the ID is already present.&lt;/p&gt;

&lt;p&gt;If you expose a unified &lt;code&gt;.save()&lt;/code&gt; method to the outer layers, the underlying framework is forced to run a hidden, expensive &lt;code&gt;SELECT&lt;/code&gt; query just to check whether the ID already exists in the database before deciding to execute an &lt;code&gt;INSERT&lt;/code&gt; or an &lt;code&gt;UPDATE&lt;/code&gt;. By explicitly splitting these actions in our core contract, we remove this infrastructure ambiguity entirely:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CrudDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Return the object or null. The business layer will decide how to proceed&lt;/span&gt;
    &lt;span class="no"&gt;E&lt;/span&gt; &lt;span class="nf"&gt;loadById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt; &lt;span class="n"&gt;id&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;Unlike standard frameworks that blindly force you to wrap your lookups in &lt;code&gt;Optional&lt;/code&gt;, our core interface relies on clean, raw type returns. &lt;code&gt;Optional&lt;/code&gt; at the database level is a costly abstraction. It doesn't eliminate missing values; it just masks them behind wrapper objects, eating up your heap allocation and putting unnecessary pressure on the Garbage Collector in high-throughput enterprise systems. If an object is missing, the database layer should return &lt;code&gt;null&lt;/code&gt;. It is up to the business context on the invocation side to decide whether that missing record warrants a &lt;code&gt;NotFoundException&lt;/code&gt; or signals an entry point for an initialization flow.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Type-Safe Pagination and Sorting Without Spring
&lt;/h3&gt;

&lt;p&gt;How do we support enterprise-grade searching, pagination, and sorting without importing &lt;code&gt;org.springframework.data.domain.Pageable&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;We define our own lightweight, framework-agnostic parameter objects. To enforce compile-time safety on sorting and prevent malicious SQL injections via arbitrary string sorting parameters, we introduce the &lt;code&gt;SortBy&lt;/code&gt; interface:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SortBy&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getPropertyName&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;Every domain can now declare its own concrete sorting capabilities using type-safe Java Enums:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;ProductSort&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;SortBy&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;PRICE&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="no"&gt;CREATED_AT&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"createdAt"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="no"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&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;String&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;ProductSort&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;propertyName&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;propertyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;propertyName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getPropertyName&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;propertyName&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;Using this, our base search query transport object remains incredibly clean, pure, and descriptive:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractParams&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;S&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SortBy&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;S&lt;/span&gt; &lt;span class="n"&gt;sortBy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Direction&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Direction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DESC&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;AbstractParams&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;page&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;size&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;size&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Defining Domain-Specific Contracts
&lt;/h3&gt;

&lt;p&gt;To tie everything together, we compose these capabilities into a distinct &lt;code&gt;SearchableDao&lt;/code&gt; contract and extend it for our specific Aggregate Roots.&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SearchableDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;P&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractParams&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ResultPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;P&lt;/span&gt; &lt;span class="n"&gt;params&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;Notice how the &lt;code&gt;ProductDao&lt;/code&gt; interface lives entirely within the API module, requires zero infrastructure annotations, and can easily be extended with specialized, high-performance business queries that aren't a good fit for standard CRUD, where &lt;code&gt;ProductSearchParams&lt;/code&gt; simply extends our &lt;code&gt;AbstractParams&amp;lt;ProductSort&amp;gt;&lt;/code&gt;&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ProductDao&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt;
    &lt;span class="nc"&gt;CrudDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;SearchableDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ProductSearchParams&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Explicit business-driven query contract&lt;/span&gt;
    &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nf"&gt;findBySku&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;sku&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;
  
  
  4. Continuous Architecture: The Contract Test Blueprint
&lt;/h3&gt;

&lt;p&gt;The hidden superpower of this architecture is how trivial it makes testing. Because our DAO interfaces in this module are completely clean contracts, they allow us to implement bulletproof &lt;strong&gt;Contract Testing&lt;/strong&gt; later on.&lt;/p&gt;

&lt;p&gt;While the &lt;code&gt;dao-api&lt;/code&gt; module itself contains zero code execution and thus carries no tests, it defines the absolute baseline behaviors. In the next chapter—when we build the concrete database implementation module—we won't just write random tests for our repositories. Instead, we will construct an abstract test suite containing 15 to 20 foundational scenarios: covering successful creation, updates, constraint violations, optimistic locking behaviors, and sorting correctness.&lt;/p&gt;

&lt;p&gt;If someone breaks the data contract during a database migration or framework upgrade, the contract test will fail immediately before the code ever leaves the infrastructure module.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;By decoupling our data contracts, our domain module remains pristine, bulletproof, and completely independent. Frameworks are put back where they belong: as external delivery mechanisms.&lt;/p&gt;

&lt;p&gt;In the next chapter, we will finally bring in the heavy machinery. We will implement the concrete &lt;strong&gt;Infrastructure Database Module&lt;/strong&gt;, plug in Hibernate and Spring Data JPA under the hood, and look into how to seamlessly map database entities back to our rich domain models without losing encapsulation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the baseline state established in Chapter 3, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-03-dao-api&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-03-dao-api" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;

&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/anatomy-of-the-domain-module-throw-away-anemic-models-and-ban-manytomany-chapter-2-31ff"&gt;Read Chapter 2: Anatomy of the Domain Module&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/mapping-strategies-without-the-magic-chapter-4-25e4"&gt;Read Chapter 4: Mapping Strategies Without the Magic&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Domain Module: Throw Away Anemic Models and Ban `@ManyToMany` (Chapter 2)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:37:53 +0000</pubDate>
      <link>https://dev.to/kamenivanov/anatomy-of-the-domain-module-throw-away-anemic-models-and-ban-manytomany-chapter-2-31ff</link>
      <guid>https://dev.to/kamenivanov/anatomy-of-the-domain-module-throw-away-anemic-models-and-ban-manytomany-chapter-2-31ff</guid>
      <description>&lt;p&gt;Years ago, when I was starting my career, I religiously followed the standard textbooks. The ones that teach you that entities should be anemic (just data holders) and that the "service layer" should orchestrate everything.&lt;/p&gt;

&lt;p&gt;The result? After just а couple of months in production, the &lt;code&gt;@Service&lt;/code&gt; classes bloated into thousands of lines of spaghetti code, filled with endless validation checks, state mutations, and structural mess. The domain entities completely lost their integrity—anyone could mutate their state from anywhere.&lt;/p&gt;

&lt;p&gt;And if you throw physical mappings like @ManyToMany into the data access layer later? You are guaranteed to face N+1 query issues, tight coupling, and a schema so tangled that splitting it into microservices feels like performing open-heart surgery.&lt;/p&gt;

&lt;p&gt;In this article, we will break down the design of our &lt;code&gt;domain&lt;/code&gt; module. It is written in &lt;strong&gt;modern Java&lt;/strong&gt;, remains completely free of Spring dependencies, guarantees encapsulation through a &lt;strong&gt;Rich Domain Model&lt;/strong&gt;, and runs unit tests in milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pure OOP (Without Spring Data Magic)
&lt;/h2&gt;

&lt;p&gt;When writing a clean domain model, we must isolate the infrastructure. Instead of relying on Spring Data JPA’s auditing framework at such an early stage, we define the rules for creation, modification, and naming directly within our domain's Java hierarchy.&lt;/p&gt;

&lt;p&gt;Here is the blueprint of our base domain classes:&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;// Every domain object that requires creation audit tracking&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;IdType&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt; &lt;span class="n"&gt;id&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;createdAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every domain object that also tracks modification timestamps&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractUpdatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;AbstractUpdatable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every domain object requiring a full audit trail (who created and updated it)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractAuditable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractUpdatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;createdById&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;updatedById&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;AbstractAuditable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
     &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  
    &lt;span class="o"&gt;}&lt;/span&gt;  

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;AbstractAuditable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IdType&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;createdById&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; 
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;createdById&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createdById&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;updatedById&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createdById&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// At creation, updatedBy matches creator&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;h3&gt;
  
  
  💡 But wait... why initialize timestamps directly in the constructor?
&lt;/h3&gt;

&lt;p&gt;Because your domain models must be self-contained and fully functional from the very millisecond they are instantiated. Since our &lt;code&gt;domain&lt;/code&gt; module is a pure Java library with zero infrastructure awareness, we cannot and should not rely on database triggers or external framework lifecycles.&lt;/p&gt;

&lt;p&gt;By setting &lt;code&gt;Instant.now()&lt;/code&gt; directly in the constructor, we guarantee that the domain object is always complete, valid, and immediately testable in-memory, without waiting for some future persistence layer to "hydrate" its state. It also prevents &lt;code&gt;NullPointerException&lt;/code&gt; bugs when sorting or processing newly instantiated domain events in memory before database transactions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Death to &lt;code&gt;@ManyToMany&lt;/code&gt; – Designing Decoupled Relationships
&lt;/h2&gt;

&lt;p&gt;In real-world, highly scalable systems, physically coupling two independent business contexts (like &lt;code&gt;Product&lt;/code&gt; and &lt;code&gt;Category&lt;/code&gt;) via Hibernate mappings is an architectural dead-end.&lt;/p&gt;

&lt;p&gt;Therefore, in our architecture, &lt;strong&gt;&lt;code&gt;@ManyToMany&lt;/code&gt; relationships are strictly banned&lt;/strong&gt;. Instead, we model them as completely decoupled Aggregates that communicate solely through identifiers (UUIDs) using a dedicated &lt;strong&gt;Relationship Entity&lt;/strong&gt;:&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;// Aggregate Root #1: Category&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;Category&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractAuditable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;active&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;Category&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
        &lt;span class="c1"&gt;// POJO  &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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The decoupled junction: CategoryAssignment (Relationship Entity)&lt;/span&gt;
&lt;span class="c1"&gt;// Since relationship assignments are append-only, inheriting from AbstractCreatable is perfect!&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;CategoryAssignment&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractCreatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Pure UUID instead of Product&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;categoryId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Pure UUID instead of Category&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;assignedById&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// For auditing purpose, who made the association&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CategoryAssignment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;categoryId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;assignedById&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&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;categoryId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categoryId&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;assignedById&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assignedById&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why is this "Distributed Ready"?
&lt;/h3&gt;

&lt;p&gt;If tomorrow your business decides to migrate Categories to a separate microservice (e.g., a dedicated &lt;code&gt;Catalog Service&lt;/code&gt;), &lt;strong&gt;the code of your Product domain won't change by a single line&lt;/strong&gt;. The Product database will simply store the &lt;code&gt;categoryId&lt;/code&gt; as a plain UUID received via Kafka or a REST payload, without caring about the internal database tables or Hibernate proxies of the neighboring context.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. When Are Direct Relationships Allowed? (Aggregate Root &amp;amp; 1-to-1)
&lt;/h2&gt;

&lt;p&gt;While we decouple large business units via UUIDs, how do we handle tightly bound details?&lt;/p&gt;

&lt;p&gt;If an entity (e.g., &lt;code&gt;ProductSpecification&lt;/code&gt;) has no independent business meaning and shares 100% of the lifecycle of the main object (&lt;code&gt;Product&lt;/code&gt;), they become part of the same &lt;strong&gt;Aggregate Root&lt;/strong&gt;. When the product is deleted, the specification must be deleted cascadingly.&lt;/p&gt;

&lt;p&gt;Here is how we model this in our domain, where &lt;code&gt;Product&lt;/code&gt; holds a direct reference to &lt;code&gt;ProductSpecification&lt;/code&gt; but acts as the strict transactional 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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductSpecification&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractUpdatable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;weight&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;ProductSpecification&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&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;dimensions&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;weight&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&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;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weight&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Aggregate Root: Product controlling the lifecycle of its nested entities&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;Product&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractAuditable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;ProductSpecification&lt;/span&gt; &lt;span class="n"&gt;specifications&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Strongly bound 1-to-1&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&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;name&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;sku&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;price&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ProductSpecification&lt;/span&gt; &lt;span class="n"&gt;specifications&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&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;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;specifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specifications&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DRAFT&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&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;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;nextStatus&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="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;canTransitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextStatus&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Business rule violated: Cannot transition from "&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;status&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" to "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;nextStatus&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nextStatus&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;h2&gt;
  
  
  4. State Machine: Inside the Enum or in an External State Manager?
&lt;/h2&gt;

&lt;p&gt;A common debate among senior engineers is: &lt;em&gt;Where should the state transition logic live—inside the Enum or delegated to an external State Manager?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The architectural answer is clear: &lt;strong&gt;State transition rules belong to the Domain (inside the Enum), while an external State Manager/orchestrator should only handle structural side-effects.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you leak state validation rules outside, you anemicize &lt;code&gt;Product&lt;/code&gt; and allow any developer to bypass the state machine, mutating the object into an invalid state.&lt;/p&gt;

&lt;p&gt;Here is our encapsulated &lt;code&gt;ProductStatus&lt;/code&gt; enum:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;DRAFT&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Override&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;canTransitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;next&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="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;ACTIVE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;ARCHIVED&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="no"&gt;ACTIVE&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Override&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;canTransitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;next&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="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;OUT_OF_STOCK&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;ARCHIVED&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="no"&gt;OUT_OF_STOCK&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Override&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;canTransitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;next&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="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;ACTIVE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;ARCHIVED&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="no"&gt;ARCHIVED&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Override&lt;/span&gt;
        &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;canTransitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;next&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="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Terminal state. No transitions allowed!&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;canTransitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt; &lt;span class="n"&gt;next&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;What about the external State Machine? It acts purely as an orchestrator. Once the domain layer successfully performs the transition, the orchestrator commits the transactions and triggers the side-effects (e.g., publishing a &lt;code&gt;ProductActivatedEvent&lt;/code&gt; to Kafka or clearing a cache).&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The 5-Millisecond Unit Test
&lt;/h2&gt;

&lt;p&gt;Since our domain module does not import a single &lt;code&gt;@Component&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt;, or &lt;code&gt;@Autowired&lt;/code&gt; annotation, we don't need a slow boot context to validate our business rules.&lt;/p&gt;

&lt;p&gt;We test pure Java objects, and the execution completes in &lt;strong&gt;under 5 milliseconds&lt;/strong&gt;:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductDomainTestCase&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;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="no"&gt;CREATOR_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&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;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ProductSpecification&lt;/span&gt; &lt;span class="n"&gt;spec&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;ProductSpecification&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;  

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;;&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;product&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;Product&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;  
    &lt;span class="o"&gt;}&lt;/span&gt;  

    &lt;span class="c1"&gt;// --- DRAFT Status Transitions ---  &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;shouldAllowTransitionFromDraftToActive&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACTIVE&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;ProductStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACTIVE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatus&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By keeping the business layer pure, you establish a highly testable, robust, and decoupled domain core. Frameworks, databases, and network protocols are just external details plugged in at a later stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;In the next part, we will dive into the very foundation of Data Access Layer design — the &lt;strong&gt;DAO API Module&lt;/strong&gt; — and discuss how to keep it 100% pure without a single specific implementation (hibernate, mysql, oracle sql, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  Codebase &amp;amp; Architecture Blueprint
&lt;/h3&gt;

&lt;p&gt;The entire evolutionary architecture of this project is tracked using strict Git tags. To clone the repository and switch exactly to the state established in Chapter 2, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-02-domain&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-02-domain" rel="noopener noreferrer"&gt;advanced-spring-multimodule&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: All core modules are configured with strict compilation-level boundaries. Compile and run &lt;code&gt;mvn clean install&lt;/code&gt; to see the structure in action. Maven version 3.9.* and Java 25 are required.&lt;/p&gt;




&lt;p&gt;◀️ &lt;a href="https://dev.to/kamenivanov/why-package-structures-wont-save-your-spring-boot-architecture-and-how-maven-enforces-it-40k4"&gt;Read Chapter 1: Stop returning Spring Data Page from your REST endpoints&lt;/a&gt;&lt;br&gt;
▶️ &lt;a href="https://dev.to/kamenivanov/anatomy-of-the-data-access-contract-protect-your-domain-from-framework-leaks-chapter-3-ni"&gt;Read Chapter 3: The Dao API Module&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Join the Masterclass Journey
&lt;/h3&gt;

&lt;p&gt;This article is part of an ongoing weekly series. If you want to receive every upcoming chapter directly in your inbox, alongside deep-dives, infrastructure architecture diagrams, and premium insights before anyone else, &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com/subscribe" rel="noopener noreferrer"&gt;subscribe to my Substack Newsletter here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;




&lt;h3&gt;
  
  
  Recommended Resources for Java &amp;amp; Spring Boot Engineers
&lt;/h3&gt;

&lt;p&gt;If you are preparing for Senior/Lead Java interviews or looking to solidify your Spring Boot &amp;amp; Architecture skills, check out these highly-rated resources from the &lt;strong&gt;Javarevisited&lt;/strong&gt; publication &lt;em&gt;(Use promo code &lt;strong&gt;friends20&lt;/strong&gt; for an exclusive &lt;strong&gt;20% discount&lt;/strong&gt; automatically applied at checkout)&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Java Interview&lt;/strong&gt;
Prepare for core Java, concurrency, JVM internals, and design pattern questions.
&lt;a href="https://gumroad.com/a/501547619/QqjGH" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/HMOAv" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the Spring Boot Interview&lt;/strong&gt;
Master Spring Core, Auto-configuration, Spring Data JPA, Security, and Microservices.
&lt;a href="https://gumroad.com/a/501547619/hrUXKY" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/pfolo" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grokking the SQL Interview&lt;/strong&gt;
Deep-dive into query optimization, indexing, joins, and complex SQL window functions.
&lt;a href="https://gumroad.com/a/501547619/fhmehw" rel="noopener noreferrer"&gt;Get Full Book (Paid)&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/brruh" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Complete Java + Spring + SQL Interview Bundle&lt;/strong&gt;
Get all three interview guides in a single heavily discounted package.
&lt;a href="https://gumroad.com/a/501547619/gvqgjk" rel="noopener noreferrer"&gt;Get the Ultimate Interview Bundle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Professional Certification Practice Questions (250+ Questions)&lt;/strong&gt;
Validating your skills? Practice with real exam-style questions before taking the Spring Professional certification.
&lt;a href="https://gumroad.com/a/501547619/sygyq" rel="noopener noreferrer"&gt;Get the Spring Professional Questions&lt;/a&gt; | &lt;a href="https://gumroad.com/a/501547619/qelhye" rel="noopener noreferrer"&gt;Download Free Sample Copy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>springboot</category>
    </item>
  </channel>
</rss>
