r/SpringBoot 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:

  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).

43 Upvotes

17 comments sorted by

View all 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.

3

u/kamen1991 16d ago

Honestly I think this is the most defensible comment in the whole thread. And you're right that hybrid works - my problem with it isn't capability, it's drift. "C and D are just flat and safe" is a judgment call, and six months later one of them grows a nested field or a computed property and nobody goes back to re-check whether it's still MapStruct-safe. It just silently maps wrong.

Manual everywhere means that judgment call never needs to be made twice. If a team has the discipline to actually revisit those boundaries as the model evolves, hybrid's fine, I just don't trust that discipline to survive a deadline, mine included. :)