DTOs & Entity Mapping
Module 06 flagged the risk. This module is the fix, done properly, and the mapping code that comes with it.
Learning objectives
- Beginner: A single response DTO per entity, manually mapped in the service layer.
- Intermediate: Separate create/update/response DTOs per resource, since what's valid to create often differs from what's valid to update (e.g. an update might allow a partial subset of fields).
- Advanced: MapStruct-generated mappers across a large API surface, with custom mapping methods for fields needing non-trivial transformation (e.g. flattening a nested entity relationship into a single DTO field).
◆ The problem
A JPA @Entity is shaped by your database schema and ORM relationships, not by what an API consumer should see. Serializing an entity directly to JSON can leak internal fields (password hashes, audit columns), trigger lazy-loading exceptions on unloaded relationships (Module 09), and — most importantly — tightly couples your public API contract to your internal database schema, so a harmless-seeming column rename becomes a breaking API change.
A DTO is a plain object shaped specifically for what a given endpoint should expose or accept — completely decoupled from how that data happens to be stored.
@Entity public class Book { @Id @GeneratedValue private Long id; private String title; private String author; private String internalIsbnSource; // internal-only, never exposed private Instant createdAt; } // what a client is allowed to SEND when creating a book public record CreateBookRequest( @NotBlank String title, @NotBlank String author) {} // what a client SEES back — no internal fields, no way to have set createdAt public record BookResponse(Long id, String title, String author, Instant createdAt) {}
Notice CreateBookRequest has no id or createdAt field at all — it's structurally impossible for a client to set them, which is a stronger guarantee than validating and rejecting them after the fact.
💻 Code example
@Entity public class Book { @Id @GeneratedValue private Long id; private String title; private String author; private String internalIsbnSource; // internal-only, never exposed private Instant createdAt; } // what a client is allowed to SEND when creating a book public record CreateBookRequest( @NotBlank String title, @NotBlank String author) {} // what a client SEES back — no internal fields, no way to have set createdAt public record BookResponse(Long id, String title, String author, Instant createdAt) {}
@Service public class BookService { public BookResponse create(CreateBookRequest request) { Book book = new Book(); book.setTitle(request.title()); book.setAuthor(request.author()); Book saved = bookRepository.save(book); return new BookResponse(saved.getId(), saved.getTitle(), saved.getAuthor(), saved.getCreatedAt()); } }
Manual mapping (as above) is explicit and has zero magic — the trade-off is boilerplate that grows with every field. Libraries like MapStruct generate this mapping code at compile time from an interface you declare, trading a small amount of build-time setup for eliminating the boilerplate entirely while staying just as fast as hand-written code (no reflection at runtime, unlike some older mapping libraries).
@Mapper(componentModel = "spring") public interface BookMapper { BookResponse toResponse(Book book); Book toEntity(CreateBookRequest request); }
▲ Pitfall
DTO mapping is a natural place for Lombok's @Data / @Builder to reduce getter/setter/constructor boilerplate — but be careful applying @Data (which generates equals() / hashCode()) directly to a JPA @Entity: it can produce broken equality semantics for entities managed by Hibernate's persistence context. Use Lombok freely on DTOs; be deliberate about which Lombok annotations you use on entities specifically.
✓ Quick recap
What's the strongest argument for a separate CreateBookRequest DTO over reusing the entity for input? Fields the client shouldn't set (id, timestamps) simply don't exist on the DTO — structurally impossible to send, not just rejected after validation. What does MapStruct trade for eliminating manual mapping boilerplate? A small amount of build-time interface declaration, generating the mapping code at compile time rather than via runtime reflection.
💻 Code example
@Service public class BookService { public BookResponse create(CreateBookRequest request) { Book book = new Book(); book.setTitle(request.title()); book.setAuthor(request.author()); Book saved = bookRepository.save(book); return new BookResponse(saved.getId(), saved.getTitle(), saved.getAuthor(), saved.getCreatedAt()); } }
Want a visual for this concept?
Generate a diagram tailored to “DTOs & Entity Mapping” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →