<?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>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;
▶️ Read Chapter 6: The Dao Implementation Layer (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;

</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;
▶️ Read Chapter 5: The Lombok Illusion (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;

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