Why this matters
- Exposing
@Entityobjects on REST APIs leaks internal fields, creates tight coupling, and enables lazy-loading exceptions mid-serialisation. - Records (Java 16+) eliminate boilerplate for request and response types.
- Separate read and write DTOs prevent clients from overwriting fields they should not touch (like
idorcreatedAt).
Why not return entities?
// BAD: entity on the wire
@GetMapping("/{id}")
public Book get(@PathVariable Long id) {
return bookService.findById(id); // exposes all fields, lazy relations
}
Problems:
Why entities must not leak to HTTP
- Internal fields (
version, audit columns) become visible. - Changing the database schema breaks API clients.
- Lazy-loaded
@ManyToOnerelations triggerLazyInitializationExceptionduring JSON serialisation.
Response DTO as a record
public record BookResponse(
Long id,
String title,
String isbn,
BigDecimal price
) {
public static BookResponse from(Book book) {
return new BookResponse(
book.getId(), book.getTitle(), book.getIsbn(), book.getPrice());
}
}
Immutable, concise, and serialisable by Jackson without configuration.
Request DTOs: separate create and update
public record CreateBookRequest(
@NotBlank String title,
@NotBlank @Pattern(regexp = "\\d{13}", message = "ISBN must be 13 digits")
String isbn,
@NotNull @Positive BigDecimal price
) {}
public record UpdateBookRequest(
@NotBlank String title,
@NotNull @Positive BigDecimal price
) {}
Create includes isbn; update does not — clients cannot change an ISBN after creation.
DTO design rules for BookStore
- One DTO per use case —
CreateBookRequest,UpdateBookRequest,BookResponse,BookSummary. - Records for immutability — No setters means no accidental mutation after validation.
- Validation on input DTOs —
@NotBlank,@Positiveon request records, not entities. - Static factory
from()— Map entity to response in one place.
Mapping in the controller
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public BookResponse create(@RequestBody @Valid CreateBookRequest request) {
Book book = bookService.create(request);
return BookResponse.from(book);
}
Controller converts inbound DTO → service call → outbound DTO. Service works with entities.
Mapping in the service
When mapping logic grows beyond a one-liner:
@Service
public class BookService {
public Book create(CreateBookRequest request) {
Book book = new Book(request.title(), request.isbn(), request.price());
return repository.save(book);
}
}
For complex mappings across many fields, consider MapStruct:
@Mapper(componentModel = "spring")
public interface BookMapper {
BookResponse toResponse(Book book);
Book toEntity(CreateBookRequest request);
}
MapStruct generates implementation at compile time — no reflection overhead.
List endpoints with summary DTOs
Full BookResponse for detail views; lighter BookSummary for lists:
public record BookSummary(Long id, String title, BigDecimal price) {
public static BookSummary from(Book book) {
return new BookSummary(book.getId(), book.getTitle(), book.getPrice());
}
}
@GetMapping
public List<BookSummary> list() {
return bookService.findAll().stream()
.map(BookSummary::from)
.toList();
}
Smaller payloads, faster serialisation, less bandwidth.
Nested DTOs
public record AuthorResponse(Long id, String name) {}
public record BookDetailResponse(
Long id,
String title,
String isbn,
BigDecimal price,
AuthorResponse author
) {
public static BookDetailResponse from(Book book) {
return new BookDetailResponse(
book.getId(), book.getTitle(), book.getIsbn(), book.getPrice(),
new AuthorResponse(book.getAuthor().getId(), book.getAuthor().getName())
);
}
}
Fetch the author eagerly in the service to avoid lazy-loading during mapping.
Quick recall
Everything you need if you only revisit this box.
- DTOs decouple the API contract from the database schema.
- Java records are ideal for immutable request/response types.
- Separate DTOs per use case: create, update, detail, summary.
- Map entities to DTOs via static
from()factories or MapStruct. - Validate input on request DTOs with
@Validand Bean Validation annotations. - Never return entities — lazy relations cause serialisation failures.
Test yourself
Answer these before moving on — recall is what makes it stick.