Keeping persistence details from leaking into business services - a generic DAO layer that actually holds up

Working through a multi-module architecture for an enterprise Java service, and the thing I keep fighting is the same thing every team eventually fights: JPA annotations, pagination params, and query logic quietly leaking out of the persistence layer and into services and domain models until nothing's testable without a database running.

The approach that's worked so far: a generic CrudDaoImpl/SearchableDaoImpl hierarchy that every concrete DAO extends, so search/pagination/spec-building logic lives in exactly one place instead of copy-pasted per entity. The domain objects stay pure Java - no framework annotations, immutable audit fields, no setters and the DAO layer is the only thing allowed to touch things like createdAt/updatedAt, set explicitly in code rather than trusted to a DB default (Hibernate doesn't read back DB-computed defaults after save(), so relying on the default alone hands you a null timestamp on the object you just created, I found that one the hard way).

The part I didn't expect to spend as much time on: some entities are append-only and never get an updatedAt at all, so the base class can't statically assume every entity supports it. Ended up with an explicit instanceof check in the shared preCreate/preUpdate hooks rather than forcing every entity through an interface it doesn't need, I felt it like a compromise at first but it reads as the right call in hindsight.

Longer writeup with the actual class hierarchy and repository access: (link in comments)

How do others handle the "some entities need X, some don't" problem in a shared base class without it turning into an interface explosion?

reddit.com
u/kamen1991 — 23 hours ago

Lombok is a case study in borrowing against your future debugging time

Not trying to restart the holy war, just a framing I keep coming back to: Lombok saves lines today by adding a compile-time dependency every other tool in your pipeline now has to agree on IDE indexing, static analysis, OpenAPI generation, whatever else is running in the build, CI on a different JDK than your laptop. Then someone bumps a JDK or IDE version, a generated builder or equals stops matching what everyone assumed, and you lose half a day to a disagreement between tools, not an actual bug.

The other thing: a getter is public API, a setter is a mutation point, equals/hashCode define identity. Those are decisions, not boilerplate. Generating them doesn't remove the decision, just your visibility into it.

Wrote this up as chapter 5 of a series on building a Spring/Java system module by module. Link in comments. Curious if anyone's got a counterexample where it was worth it at scale, not just at the PR-diff level.

reddit.com
u/kamen1991 — 8 days ago

Why I stopped putting Lombok anywhere near my enterprise projects

Once I got a call a while back because a customer record "wasn't processing." Two hours later the culprit turned out to be log.warn("Could not process customer: {}", customer) - @Data's generated toString() was walking a lazy @OneToMany, the persistence context was already closed, and the log line itself threw the exception. The stack trace pointed at logging code, not the real bug.

That's one reason why I don't put @Data on entities anymore. Field-based equals is wrong for most entities, a toString that walks associations is a logging incident waiting to happen, and once you're bolting @ToString(exclude = ...) back on to undo it, you haven't saved typing, you've just hidden it.

Wrote up the longer version (builders, @SuperBuilder gotchas) as chapter 5 of a series I'm doing on Spring/Java architecture. Link in comments if anyone wants it. Curious if others have hit the same lazy-loading-in-logs thing or if it's just how our entities were modeled.

reddit.com
u/kamen1991 — 8 days ago

Automated mapping frameworks quietly reward keeping your layers identical - even when they shouldn't be

Something that's bugged me for a while is that automated mapping tools (MapStruct, AutoMapper, ModelMapper - whatever your language's equivalent is) work best when your Domain Model, DB Entity, and external DTO all look basically the same. Which is exactly the case where you don't need a separate domain, entity, and DTO in the first place.

The moment those three actually diverge (which they should, because each protects something different) - the tooling starts fighting you. Custom `@Named` helpers, Java code embedded inside annotation strings, string-based property paths that don't survive an IDE rename. At that point you're maintaining configuration to compensate for a tool encouraging the wrong shape.

We ended up dropping MapStruct entirely in a multi-module Java project and writing explicit transformers instead — plain Java, no annotation processor, no generated code. More files, more typing, but 100% refactoring safety and nothing hidden between your domain code and what's actually running.

Curious how others have handled this tension, especially in polyglot orgs where "just write a mapper" isn't as trivial as it sounds and the tool itself becomes an architectural pressure toward convergence.

(Wrote a longer breakdown with concrete examples - composite value objects, unit conversions, structural mismatches - link in comments)

reddit.com
u/kamen1991 — 15 days ago

How we structure Entity/DTO mapping in a multi-module Spring Boot project (without MapStruct)

Something has always bugged me about relying on annotation-based mapping frameworks once a Spring Boot project grows past a few modules. MapStruct is miles ahead of dynamic tools like ModelMapper thanks to compile-time code generation, but we kept running into recurring friction as our domain, entity, and DTO layers diverged.

That's why we ended up dropping MapStruct entirely in favor of plain Java transformer classes. No annotation processor, no generated sources, no separate mapper interface per entity pair.

The reasons that pushed us there:

  1. Fragile IDE refactoring: string path mappings like `@Mapping(source = "shippingDetails.address.street", target = "street")` don't reliably survive a rename. You usually catch it during the build, sometimes later.
  2. Annotation pollution for anything non-trivial: once you need a custom transformation, you're writing @Namedhelpers or embedding Java inside annotation strings likeexpression = "java(...)".
  3. Debugging noise: stepping through target/generated-sources instead of your own domain code.

The trade-off is real - more files, more explicit code to write. What we get back is full IDE refactoring safety, no annotation-processor step in the build, and a debugger that only ever shows real code.

Anyone else moved off MapStruct in a modular Spring Boot setup, or is this more trouble than it's worth for most projects?

(I Wrote a deeper architectural breakdown with code samples if anyone is interested - link in comments).

reddit.com
u/kamen1991 — 15 days ago

Why package structures fail in Spring Boot (and how we turned architecture rules into Maven compilation errors)

Hey everyone! I just published a deep dive into solving a classic enterprise problem: how junior or stressed developers bypass package separation (.controller, .service, .repository) under tight deadlines.

Instead of relying on folder structures and code reviews, we split our project into strict Maven modules (isolating core domain and business logic from frameworks like JPA or Kafka). If someone tries to inject an EntityManager where it doesn't belong, the code simply will not compile.

  • The Topology: Split into independent modules like domain, business-logic, dao-api, and dao-impl.
  • The Result: Zero cyclic dependencies, lightning-fast unit tests, and eliminated architectural decay.

(I'm dropping the full article link in the comments for anyone interested in the code breakdown.)

reddit.com
u/kamen1991 — 22 days ago

@ManyToMany in enterprise domains (and why your ORM is coupling your bounded contexts)

In Clean Architecture and DDD, we all agree that frameworks and databases are external details. Yet, I still see enterprise apps embedding physical ORM mappings directly into core domain models.

Specifically, relying on physical `@ManyToMany` annotations for large-scale systems is an architectural trap:

  • It tightly couples database schemas across completely different business contexts.
  • It guarantees N+1 query surprises and LazyInitializationExceptions down the road.
  • It turns future microservices decomposition into a nightmare.

In the second part of my engineering series, I break down how and why we handle this differently:

  • Rich Domain Models: Keeping business logic and invariants inside the Aggregate Roots, instead of scattering them across anemic `@Service` classes.
  • Distributed-Ready Relationships: Swapping ORM join tables for explicit relationship models holding UUID references and audit metadata.
  • Framework Isolation: Enforcing strict Maven module boundaries so the domain core stays 100% pure Java, completely decoupled from Hibernate or Spring Data.

I'm curious to hear how you handle this, do you still trust Hibernate to manage complex relationships out-of-the-box, or have you moved to explicit join entities?

(I'm dropping the full article link in the comments for anyone interested in the code breakdown.)

reddit.com
u/kamen1991 — 23 days ago

Moving past the illusion of package control: Enforcing Clean Architecture via build tools

Package structures (.controller, .service, .dao) give us an illusion of control, but they don't stop anyone from writing raw SQL in a REST controller when a deadline looms.

In my first article, I share how we turned architectural guidelines into compilation errors by utilizing a multi-module Maven dependency topology.

  • The Topology: Split into independent modules like domain, business-logic, dao-api, and dao-impl.
  • The Result: Zero cyclic dependencies, lightning-fast unit tests, and eliminated architectural decay.

Read the full breakdown and explore the implementation code in the first comment.

reddit.com
u/kamen1991 — 27 days ago