The DTO Pattern
A Data Transfer Object (DTO) is a simple, purpose-built object that carries data across a boundary — typically between your web layer and clients. Instead of returning JPA @Entity classes straight from a controller, you map them to DTOs that describe exactly what an API should accept and return. This small layer of indirection pays off in decoupling, security, and long-term API stability.
What a DTO is
A DTO has no behaviour beyond holding data. It is a flat, serializable bag of fields tailored to a single use case — one shape for what a client sends, another for what it receives. DTOs contain no persistence annotations, no business logic, and no Hibernate proxies.
public record ProductResponse(
Long id,
String name,
BigDecimal price,
String categoryName
) {}
That record maps cleanly to JSON and exposes only the four fields a client actually needs — not the entire database row.
Why not expose entities directly?
It is tempting to annotate an entity with @Entity and return it from a @RestController. It works in a demo, but it couples your public API to your database schema and leaks problems. The DTO pattern solves four concrete issues.
Decoupling the API from the schema
Entities model your database; DTOs model your API contract. When you rename a column, split a table, or add an internal audit field, the entity changes — but the DTO (and therefore every client) stays stable. Without DTOs, a routine schema migration becomes a breaking API change.
Security — don’t leak sensitive fields
Entities frequently hold data clients must never see: password hashes, internal status flags, soft-delete markers, foreign keys. Serializing an entity ships all of it by default.
@Entity
public class User {
@Id private Long id;
private String email;
private String passwordHash; // must never reach a client
private boolean internalFlag; // implementation detail
}
A UserResponse DTO simply omits the fields you don’t want to expose, making leaks impossible by construction rather than by remembering to add @JsonIgnore everywhere.
API stability
A DTO is an explicit, reviewable contract. Adding a field to an entity does not silently expand your JSON payload, and consumers depend on a shape you control deliberately. Request DTOs also guard the inbound side: a client cannot mass-assign id, createdAt, or role just because those columns exist.
Avoiding lazy-loading serialization issues
This is the classic entity-serialization trap. When Jackson serializes an entity with a LAZY association outside an open transaction, Hibernate throws LazyInitializationException — or worse, eagerly walks a @OneToMany graph and serializes your entire object tree (often with infinite recursion).
com.fasterxml.jackson.databind.JsonMappingException:
could not initialize proxy [com.acme.Order#42] - no Session
A DTO sidesteps this entirely: you map only the loaded fields you need, so there are no proxies and no surprise SQL during serialization. See N+1 queries for the related fetching pitfall.
Request vs response DTOs
Use separate DTOs for input and output. They almost never have the same shape, and conflating them invites security holes.
| Aspect | Request DTO | Response DTO |
|---|---|---|
| Direction | Client → server | Server → client |
| Typical name | CreateProductRequest | ProductResponse |
Includes id? | No (server-assigned) | Yes |
| Includes timestamps? | No | Often (createdAt) |
| Validation | @NotBlank, @Positive, etc. | None needed |
| Maps to | Used to build/update an entity | Built from an entity |
public record CreateProductRequest(
@NotBlank String name,
@Positive BigDecimal price,
@NotNull Long categoryId
) {}
The request DTO carries @Valid constraints and deliberately omits id; the response DTO includes the server-generated id and any computed fields. See Validating the request body for wiring up @Valid.
Tip: Keep DTOs close to the layer that uses them. A common layout is
web/dto/requestandweb/dto/responsepackages, leaving thedomainorentitypackage free of any HTTP concerns.
Note: A DTO is not the same as a domain model. DTOs are dumb data carriers at the edge; your entities and domain objects still hold the real model and behaviour.
A complete flow
The controller speaks only DTOs; the service performs the mapping and works with entities internally.
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService service;
@PostMapping
public ResponseEntity<ProductResponse> create(
@Valid @RequestBody CreateProductRequest request) {
ProductResponse created = service.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/{id}")
public ProductResponse byId(@PathVariable Long id) {
return service.findById(id);
}
}
The web layer never sees a Product entity. How that mapping happens — by hand or with a library — is the subject of the rest of this section.
In This Section
- Entity vs DTO — a side-by-side comparison and when a DTO is overkill.
- Manual Mapping — hand-written mapper methods and static factories.
- MapStruct — compile-time generated mappers.
- ModelMapper — runtime reflection-based mapping.
- Records as DTOs — using immutable Java records for DTOs.