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?