Foundation · Build
DTOs vs Entities: Mapping at the Boundary
Give your public contract its own shape instead of leaking JPA entities through controllers, and map explicitly where the models diverge.
Primary source: Spring MVC: Mapping Request and Response Payloads
Lesson 7 of 11
1d streakThe Architectural Question
Entities returned directly couple clients to table shapes, leak columns you never meant to publish, and can fire lazy queries during JSON serialization that crash with detached errors in production-only code paths.
What this guide proves
- Keep request and response DTOs independent of persistence annotations
- Map between boundary and domain models in one reviewed place
- Evolve the API without turning internal refactors into breaking changes
Give the API and persistence model different jobs
package com.example.tasks.task;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.Instant;
record CreateTaskRequest(
@NotBlank @Size(max = 120) String title
) {}
record TaskResponse(long id, String title, String status) {}
// Persistence-only shape: never returned by a controller.
class TaskEntity {
Long id;
String title;
TaskStatus status;
Instant updatedAt;
String internalOwnerId;
}
enum TaskStatus { OPEN, DONE }Request DTOs express what clients may submit. Response DTOs express what clients may observe. Entities express persistence identity, relationships, and storage concerns. Similar fields do not make these models interchangeable: their reasons to change are different.
Entity serialization is accidental database access
Keep mapping explicit and independently testable
package com.example.tasks.task;
final class TaskMapper {
private TaskMapper() {}
static TaskEntity toEntity(CreateTaskRequest request) {
var entity = new TaskEntity();
entity.title = request.title().trim();
entity.status = TaskStatus.OPEN;
return entity;
}
static TaskResponse toResponse(TaskEntity entity) {
return new TaskResponse(entity.id, entity.title, entity.status.name());
}
}Use it when
Use handwritten pure mappings when transformations contain policy, types genuinely diverge, or the boundary is small enough to review directly.
Reconsider when
Adopt generated mapping only when repetition is measurable; keep explicit tests and forbid silent unmapped fields so automation does not hide contract drift.
Evolve the public contract without coupling it to a migration
- 01Capture representative JSON fixtures or HTTP contract tests before changing a model.
- 02Rename an internal entity field and update only the mapper; the JSON property remains stable.
- 03Add optional response data additively when existing clients can ignore it.
- 04Use a new request or response type when create, update, and read semantics diverge.
- 05Treat field removal, meaning changes, and new required input as compatibility decisions rather than refactors.
Active Recall Checkpoint
A database column and entity field must be renamed, but deployed clients still require JSON property title. What contains the change?
Hint: The public representation and persistence model do not need the same field name.
Show answer & explanation
Keep TaskResponse.title stable and change the entity plus explicit mapper behind it. A DTO boundary lets the database migration proceed without turning an internal rename into an API break.
Working Implementation
Code Walkthrough
Replace entity-returning endpoints with CreateTaskRequest and TaskResponse records plus a mapper, then break serialization deliberately with a lazy field.
package com.example.mapping;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.time.Instant;
/**
* Demonstrates DTO vs Internal Entity separation and Tolerant Reader API evolution.
*/
public class DtoMappingLab {
// Internal JPA Entity: contains internal IDs, salt/hash, audit columns
public static class UserEntity {
public Long id;
public String email;
public String passwordHash;
public Instant createdTimestamp;
}
// Public API Request DTO: Tolerant reader ignores new upstream JSON properties
@JsonIgnoreProperties(ignoreUnknown = true)
public record RegisterUserRequest(String email, String rawPassword) {}
// Public API Response DTO: NEVER exposes internal database IDs or password hashes!
public record UserResponse(String email, Instant memberSince) {
public static UserResponse fromEntity(UserEntity entity) {
return new UserResponse(entity.email, entity.createdTimestamp);
}
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Controllers accept and return records dedicated to the HTTP contract
- One mapper owns conversion so renaming a column stays invisible to clients
- No managed entity escapes the service layer into message conversion
Key Takeaway
DTOs give request, response, and persistence models independent reasons to change; explicit mapping makes that boundary reviewable and testable.
Architectural & Technical Review
Core Interview Questions
Q01Why should a controller not serialize a JPA entity directly?Show answer
It couples the API to persistence shape, can leak internal fields, and may trigger lazy database access or fail after the persistence context closes.
Q02When is handwritten mapping preferable to generated mapping?Show answer
It is preferable when transformations carry policy, the types diverge materially, or the boundary is small enough that direct, pure code is clearest to review.
Q03How can an entity field rename avoid breaking JSON clients?Show answer
Keep the response DTO property stable and change only the entity and explicit mapper, with an HTTP contract test proving the serialized shape.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Creating one universal DTO — Create, update, list, and detail operations often have different required fields and disclosure rules; one type accumulates ambiguous nulls.
- Pitfall: Using reflection mapping without drift checks — Convention mapping can silently ignore or expose fields unless unmapped properties fail the build and contract tests pin the result.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources