<?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>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;
▶️ Read Chapter 9: The Core Business Logic Module (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 Reading &amp;amp; Interview Preparation
&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;

</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;

</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;

</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;

</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;




</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Anatomy of the 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;p&gt;📨 &lt;strong&gt;Liked this architecture blueprint?&lt;/strong&gt; This article is part of my &lt;strong&gt;Evolutionary Architecture&lt;/strong&gt; series. I publish deep-dive technical pieces every week. &lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com" rel="noopener noreferrer"&gt;Subscribe to my Substack Newsletter&lt;/a&gt;&lt;/strong&gt; to get full source code repositories (Git tags) and new chapters straight to your inbox!&lt;/p&gt;

</description>
      <category>java</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Why Package Structures Won't Save Your Spring Boot Architecture (And How Maven Enforces It) (Chapter 1)</title>
      <dc:creator>Kamen</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:29:50 +0000</pubDate>
      <link>https://dev.to/kamenivanov/why-package-structures-wont-save-your-spring-boot-architecture-and-how-maven-enforces-it-40k4</link>
      <guid>https://dev.to/kamenivanov/why-package-structures-wont-save-your-spring-boot-architecture-and-how-maven-enforces-it-40k4</guid>
      <description>&lt;h2&gt;
  
  
  How We Turned Architectural Guidelines Into Compilation Errors
&lt;/h2&gt;

&lt;p&gt;Have you ever found yourself doing a Friday afternoon Code Review, only to discover that someone injected the &lt;code&gt;EntityManager&lt;/code&gt; directly into a REST controller, writing raw SQL strings, and mapping rows manually with a loop? Or worse, have you seen a &lt;code&gt;@RequestParam&lt;/code&gt; accepting &lt;code&gt;org.springframework.data.domain.Pageable&lt;/code&gt; while the controller returns a raw &lt;code&gt;Page&amp;lt;Entity&amp;gt;&lt;/code&gt; directly to the frontend?&lt;/p&gt;

&lt;p&gt;Congratulations. You have just exposed your database schema to the entire world and completely bypassed the concept of an &lt;strong&gt;Anti-Corruption Layer (ACL)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Package Control
&lt;/h2&gt;

&lt;p&gt;When an application is structured within a single module and we rely strictly on package separation (&lt;code&gt;.controller&lt;/code&gt;, &lt;code&gt;.service&lt;/code&gt;, &lt;code&gt;.repository&lt;/code&gt;), we live in an illusion of architectural control. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The hard truth:&lt;/strong&gt; Packages do not stop anyone. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the heat of a tight deadline, when &lt;em&gt;"things just need to work,"&lt;/em&gt; Java package visibility rules will not prevent a junior or stressed developer from committing architectural crimes that you will be debugging for months.&lt;/p&gt;

&lt;p&gt;Here is how we solved this problem in our team by breaking down the system into &lt;strong&gt;highly specialized Maven modules&lt;/strong&gt;, effectively turning architectural guidelines into compilation errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architectural Blueprint: Divide, Conquer, and Version
&lt;/h2&gt;

&lt;p&gt;The first step toward true isolation was radical: &lt;strong&gt;we extracted the API contract into a completely separate Git repository.&lt;/strong&gt; Why? Because your API contract version should be independent of your backend implementation. If we fix a bug tomorrow in the core business logic, it makes absolutely no sense to bump the version of the REST/Event contract if nothing changed there. The frontend team and QA engineers need a stable contract to work against, completely shielded from our internal refactoring.&lt;/p&gt;

&lt;p&gt;Next, we split the main backend project into highly specialized Maven modules. When I first proposed this, the team was highly skeptical: &lt;em&gt;"It's too complex,"&lt;/em&gt; &lt;em&gt;"Why do we need this? We already have packages."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So, I built a quick proof-of-concept. Instead of relying on a developer’s goodwill, we shifted the enforcement of architectural boundaries directly to the compiler.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dependency Topology
&lt;/h3&gt;

&lt;p&gt;Here is what the real dependency topology looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── RestService API (Git Repo 1)
│ &amp;nbsp; ├── dto (Jackson &amp;amp; Swagger)
│ &amp;nbsp; └── events (Event Contracts)
│ &amp;nbsp; └── rest-api (The API intefaces from which documentation is generated)
│
└── Backend (Git Repo 2)
    ├── domain (Pure Domain Models &amp;amp; Core Logic (No Frameworks))
&amp;nbsp; &amp;nbsp; ├── business-logic (Core Business Logic (Depends only on domain &amp;amp; APIs))
&amp;nbsp; &amp;nbsp; ├── dao-api (Database Access Interfaces (No JPA/Spring Data))
&amp;nbsp; &amp;nbsp; ├── dao-impl (Actual DB Integration (Spring Data JPA, Hibernate))
&amp;nbsp; &amp;nbsp; ├── bridge-api (External Services Communication APIs)
&amp;nbsp; &amp;nbsp; ├── bridge-impl (Actual Integration with External APIs)
&amp;nbsp; &amp;nbsp; ├── integration-tests (Testing layer via Testcontainers (Docker-based))
&amp;nbsp; &amp;nbsp; └── application (Spring Boot Bootstrapper)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this structure, the heart of the system — the &lt;code&gt;business-logic&lt;/code&gt; module — depends strictly on the interfaces defined in &lt;code&gt;dao-api&lt;/code&gt; and &lt;code&gt;bridge-api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It has &lt;strong&gt;zero access&lt;/strong&gt; to &lt;code&gt;dao-impl&lt;/code&gt; or &lt;code&gt;bridge-impl&lt;/code&gt;. Your core business logic does not have &lt;code&gt;spring-boot-starter-data-jpa&lt;/code&gt;, Hibernate, Kafka, or Redisson in its classpath.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let Maven Keep Your Code Reviews Clean
&lt;/h2&gt;

&lt;p&gt;Once we introduced this change, it didn’t take long for the team to realize its power.&lt;/p&gt;

&lt;p&gt;If a developer attempts to inject the &lt;code&gt;EntityManager&lt;/code&gt; or write raw SQL queries inside the core business logic tomorrow, &lt;strong&gt;the code simply will not compile&lt;/strong&gt;. The build will break right on their local machine. &lt;/p&gt;

&lt;p&gt;To circumvent this, they would have to deliberately go into the &lt;code&gt;pom.xml&lt;/code&gt; of &lt;code&gt;business-logic&lt;/code&gt; and introduce a dependency on the database module — an action that would instantly trigger a massive red flag during any Code Review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Immediate Benefits in an Enterprise Environment
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No More Cyclic Dependencies:&lt;/strong&gt; Maven physically forbids module A from depending on B if B already depends on A.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightning-Fast Unit Tests:&lt;/strong&gt; Because the business logic is entirely decoupled from infrastructure frameworks, unit tests are written effortlessly. We only mock pure Java interfaces, and the tests execute in milliseconds. No one can use the &lt;em&gt;"tests take too much time"&lt;/em&gt; excuse anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure Infrastructure Interchangeability:&lt;/strong&gt; The &lt;code&gt;business-logic&lt;/code&gt; module interacts solely with the contract in &lt;code&gt;dao-api&lt;/code&gt;. It doesn't know — nor does it care — whether the data underneath comes from MySQL (via &lt;code&gt;dao-impl&lt;/code&gt;), is cached in Redis, or is being streamed via Kafka (via &lt;code&gt;bridge-impl&lt;/code&gt;). The implementations are wired together at the very top layer — in the &lt;code&gt;application&lt;/code&gt; module.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion: Is it Overengineering?
&lt;/h2&gt;

&lt;p&gt;If you are building a small CRUD app with five tables, this approach is undoubtedly overengineering. But if you are building an Enterprise system designed for long-term maintainability, high team velocity, and strict domain boundaries, you cannot afford to build your house on sand.&lt;/p&gt;

&lt;p&gt;Relying purely on folder structures means that sooner or later, under pressure, someone will break the rules. Transitioning to a multi-module design requires more initial boilerplate, but it &lt;strong&gt;eliminates 70% of long-term architectural decay&lt;/strong&gt;.&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 this design — the &lt;strong&gt;Domain Module&lt;/strong&gt; — and discuss how to keep it 100% pure (POJO) without allowing a single JPA or Hibernate annotation to pollute your business models.&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 1, use the following link:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository (Tag: &lt;code&gt;chapter-01-baseline&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/KamenIvanov/advanced-spring-multimodule/tree/chapter-01-baseline" 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;▶️ Read Chapter 2: &lt;a href="https://dev.to/kamenivanov/anatomy-of-the-domain-module-throw-away-anemic-models-and-ban-manytomany-chapter-2-31ff"&gt;The Domain module&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;📨 &lt;strong&gt;Liked this architecture blueprint?&lt;/strong&gt; This article is part of my &lt;strong&gt;Evolutionary Architecture&lt;/strong&gt; series. I publish deep-dive technical pieces every week. &lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://kamenivanov.substack.com" rel="noopener noreferrer"&gt;Subscribe to my Substack Newsletter&lt;/a&gt;&lt;/strong&gt; to get full source code repositories (Git tags) and new chapters straight to your inbox!&lt;/p&gt;

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