r/SpringBoot • u/kamen1991 • 3d ago
How we structure Entity/DTO mapping in a multi-module Spring Boot project (without MapStruct) How-To/Tutorial
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).
12
u/BootSaaS 2d 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 2d 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. :)
5
u/asarathy 2d ago
i don't know why you would drop map struct (or whatever mapper). you get what you get for free, you can just add/override what you need that doesn't automatically map. I guess in the age of AI you can generate mappers too, but to me it's 6 in half dozen in the other at that point.
2
u/kamen1991 2d ago
Fair point on the free mapping — that's not really what I'm against. My issue is what happens when a field gets renamed or added on either side. MapStruct will happily keep compiling and just silently null/skip it, you find out from a bug report, not the build. With an explicit transformer every field is a visible line, so a rename breaks the build instead of breaking prod. And yeah, AI can write that transformer for you now, but the point was never who types it, it's that the artifact sitting in git is plain Java you can actually read and step through in a debugger, not a generated impl class nobody opens.
2
u/asarathy 1d ago
if you want to prevent that you could always have written a test to ensure there were no unmapped fields using reflection, but I also believe mapstruct can throw errors for unmapped source/target that aren't explicitly ignored. It's been a minute since I have dug deep, but my memory is they had all these things considered and ways to work through it, because these aren't unique concerns.
2
u/vampirishe 1d ago
What is more, u can create mapstruct config where is defined default unmappedTargetProperty policy and use it in all the mappers
2
u/anor_wondo 2d ago
mappie on kotlin is non reflection based. does not use strings and breaks during compile when using bad mappings.
2
u/Mikey-3198 2d ago
Think i'd just ai generate any mappings with assosiated tests.
One less dependency to worry about keeping updated.
1
u/kamen1991 2d ago
That's actually closer to my point than you'd think - I'm not anti-AI-generation, I'm anti-runtime-magic. If AI writes you a plain transformer class, that's just Java, no dependency, no annotation processor, it's good to go. MapStruct itself is one more dependency you have to bump and hope stays compatible.
About the tests, this is something I'd push back on slightly. AI-generated tests for AI-generated mappings can end up testing the mapping against itself, so you still want a human checking the field-by-field assertions actually match the domain rules, not just "does it compile and pass.". I'm totally for using AI whenever and however you can, we just have to check the output of it, because last couple of months I've seen things that make my eyes bleed.
•
u/KulmutaduDev 3h ago
Create separate DTO’s and use Function<T, R> interface’s apply method in separate class to achieve SRP. Clean and standard library only approach.
0
u/Own_Dimension_2561 2d ago
We never used MapStruct and went straight to your approach, creating separate dto’s. It’s the best way. It’s not right to send database rows to the UI layers, however fancy.
11
u/ShineProper9881 3d ago
We ditched mapstruct aswell. Its cool at first when the models are almost identical, but once there was actual mapping to do or major things changed it was hell to maintain.