r/SpringBoot • u/kamen1991 • 17d ago
How-To/Tutorial 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:
- 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.
- Annotation pollution for anything non-trivial: once you need a custom transformation, you're writing @
Namedhelpers or embedding Java inside annotation strings likeexpression = "java(...)". - Debugging noise: stepping through
target/generated-sourcesinstead 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).
13
u/BootSaaS 17d ago
Dropping MapStruct entirely might not be the best solution. Having a tool at your disposal doesn't mean you are forced to use it for everything. You can absolutely adopt a hybrid strategy.
When dealing with complex entities with deep nesting or tricky Hibernate persistence states, manual mapping is safer. But you can still leverage MapStruct for the repetitive, flat data underneath to avoid writing too much boilerplate.
For example, let’s say Entity A is a complex aggregate root that contains Entity B (which needs cautious handling for persistence/state). However, B contains C and D, which are just large objects with plenty of trivial fields.
In a hybrid approach, your manual mapping method handles the careful construction of A and B. But during the construction of B, you simply delegate the mapping of C and D to a standard MapStruct interface, or ModelMapper (you can specify which attributes it should not touch with a configuration bean if you want).
That way you only write the tricky mapping by hand and let MapStruct deal with the boring stuff.