Skip to content
Spring Boot sb dto 4 min read

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.

AspectRequest DTOResponse DTO
DirectionClient → serverServer → client
Typical nameCreateProductRequestProductResponse
Includes id?No (server-assigned)Yes
Includes timestamps?NoOften (createdAt)
Validation@NotBlank, @Positive, etc.None needed
Maps toUsed to build/update an entityBuilt 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/request and web/dto/response packages, leaving the domain or entity package 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

Last updated June 13, 2026
Was this helpful?