DTOs & Mapping
Reviewed & published by Brayan K
A DTO (Data Transfer Object) shapes exactly what crosses your API boundary. Keeping entities private — and mapping to DTOs — protects internal fields, decouples your schema, and dodges lazy-loading traps.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should know JPA entities and Java records, which make ideal DTOs. The validation lesson explains where request constraints belong.
What You'll Learn
- What a DTO is and how it differs from an entity
- Why you should not expose entities directly
- Using records as concise DTOs
- Separate request vs response DTOs
- Manual mapping between entities and DTOs
- When a mapping library (MapStruct) helps
The Big Idea — A Receipt, Not the Whole Ledger
💡 Analogy: Your entity is the company's full internal ledger — costs, margins, account notes, everything. You would never hand a customer the ledger; you give them a receipt (the DTO) that shows only what they should see. The receipt is shaped for the customer's needs, hides sensitive numbers, and stays the same even if you reorganize the ledger behind the scenes. Mapping is the act of writing the receipt from the ledger.
The DTO is your public contract; the entity is your private storage.
1️⃣ DTOs vs Entities
Define a response DTO with only the fields clients should see, and a request DTO with only what they may send. Records make these one-liners. The entity keeps internal fields like passwordHash that must never leak.
// Entities map to the DATABASE. DTOs shape what crosses the API boundary.
// A record is a concise, immutable carrier — perfect for a DTO.
// Response DTO: exactly the fields a client should see (note: NO passwordHash).
public record UserResponse(Long id, String name, String email) { }
// Request DTO: what a client may send to create a user (note: NO id).
public record CreateUserRequest(String name, String email, String password) { }
// The entity (persisted) may hold internal-only fields the API must never leak.
// @Entity
// public class User {
// Long id;
// String name;
// String email;
// String passwordHash; // internal — must NOT appear in responses
// Instant createdAt; // internal detail
// }2️⃣ Mapping Between Them
Mapping converts entity to DTO (dropping internal fields) and DTO to entity (filling server-side fields). Do it by hand for a few types, or generate it with MapStruct as mappings multiply.
import java.time.Instant;
// MANUAL mapping: explicit, dependency-free conversion in both directions.
public class UserMapper {
// entity -> response DTO (drop internal fields)
public static UserResponse toResponse(User u) {
return new UserResponse(u.getId(), u.getName(), u.getEmail());
}
// request DTO -> new entity (hash the password, set server-side fields)
public static User toEntity(CreateUserRequest req) {
User u = new User();
u.setName(req.name());
u.setEmail(req.email());
u.setPasswordHash(hash(req.password())); // never store the raw password
u.setCreatedAt(Instant.now());
return u;
}
private static String hash(String raw) { return "hashed(" + raw + ")"; }
}
// With a mapping LIBRARY (MapStruct) you instead declare an interface and the
// conversion code is GENERATED at compile time:
//
// @Mapper(componentModel = "spring")
// public interface UserMapper {
// UserResponse toResponse(User user);
// }3️⃣ Why It Matters — Hiding Internal Fields
Here is a fully runnable example: an "entity" carries a passwordHash, but mapping to a DTO ensures it never appears in the output.
import java.util.*;
import java.util.stream.*;
// PLAIN-JAVA, RUNNABLE: an entity with an internal field, mapped to a DTO that
// hides it. This is exactly why we don't serialize entities directly.
public class Main {
// "entity" with an internal field that must NOT leak
record User(Long id, String name, String email, String passwordHash) { }
// DTO: only the safe, public fields
record UserResponse(Long id, String name, String email) { }
static UserResponse toResponse(User u) {
return new UserResponse(u.id(), u.name(), u.email());
}
public static void main(String[] args) {
List<User> users = List.of(
new User(1L, "Ana", "ana@site.com", "secret-hash-1"),
new User(2L, "Ben", "ben@site.com", "secret-hash-2"));
List<UserResponse> dtos = users.stream()
.map(Main::toResponse)
.collect(Collectors.toList());
dtos.forEach(System.out::println);
System.out.println("leaks passwordHash? " + dtos.toString().contains("hash"));
}
}🧠 Quick Recall
Predict before you peek.
Answer: leaking internal fields and coupling the API to the schema (plus accidental lazy-loading).
Answer: the server generates the id; the client doesn't supply it on creation.
Answer: generates type-safe mapping code at compile time from an annotated interface, no reflection or boilerplate.
🎯 YOUR TURN — Product DTOs
Design a ProductResponse and CreateProductRequest, and a mapper that hides the internal costCents.
// YOUR TURN: design DTOs for a Product API.
// - ProductResponse (record): id, name, priceCents, category
// (the entity also has an internal "costCents" you must NOT expose)
// - CreateProductRequest (record): name, priceCents, category (NO id)
// - write a static toResponse(Product entity) that drops costCents
//
// public record ProductResponse(Long id, String name, int priceCents, String category) { }
// public record CreateProductRequest(String name, int priceCents, String category) { }
// static ProductResponse toResponse(Product p) { ... }🧩 MINI-CHALLENGE — Computed summary DTO
Map Order entities to OrderSummary DTOs that compute itemCount and totalCents.
Common Errors
- ❌ Returning entities from controllers. Leaks fields, couples schema. Fix: map to a DTO.
- ❌ Serializing lazy associations. Extra queries or LazyInitializationException. Fix: map only the needed fields into a DTO.
- ❌ One DTO for request and response. Forces awkward nullables. Fix: use separate request/response DTOs.
- ❌ Validation on entities. Mixes concerns. Fix: put constraints on request DTOs.
- ❌ Returning password/secret fields. Serious leak. Fix: never include them in response DTOs.
- ❌ Hand-mapping dozens of types. Boilerplate sprawl. Fix: adopt MapStruct.
Pro Tips
- 💡 Use records for DTOs — immutable, concise, and self-describing.
- 💡 Keep DTOs in their own package so the API contract is easy to find.
- 💡 Map at the service or controller edge, not deep in your domain.
- 💡 Let DTOs carry computed/aggregated shapes the entity doesn't store directly.
📋 Quick Reference
| Concept | Idea | Note |
|---|---|---|
| entity | @Entity, persisted | Private storage |
| DTO | data transfer object | Public contract |
| response DTO | safe outbound fields | No secrets |
| request DTO | allowed inbound fields | Often no id |
| record DTO | record Foo(...) | Immutable carrier |
| mapping | entity <-> DTO | Both directions |
| manual mapper | static toResponse(...) | Explicit, no deps |
| MapStruct | @Mapper interface | Generated code |
| validation | on request DTO | At the boundary |
📚 Resources / Keep Learning
- 📘 Records — Java 21 language guide (docs.oracle.com)
- 📗 MapStruct reference guide (mapstruct.org)
- 📗 Spring Data JPA — projections & DTOs (docs.spring.io)
🎉 Lesson Complete!
You now know why entities stay private, how DTOs shape your API contract, why records make ideal DTOs, how to map between entities and DTOs by hand, and when a tool like MapStruct earns its keep.
Practice quiz
What does DTO stand for?
- Data Transfer Object
- Database Table Object
- Direct Type Operation
- Domain Transaction Object
Answer: Data Transfer Object. DTO stands for Data Transfer Object, a simple object used to carry data across boundaries.
Why is it risky to expose JPA entities directly in API responses?
- It is faster but illegal
- It can leak internal fields and couple the API to the schema
- Entities cannot be serialized
- It improves security
Answer: It can leak internal fields and couple the API to the schema. Exposing entities can leak sensitive/internal fields and tightly couples your public API to the database schema.
A DTO is best described as...
- A database table
- A Spring bean lifecycle
- A shape tailored to a specific API request or response
- A validation engine
Answer: A shape tailored to a specific API request or response. A DTO is a purpose-built shape for transferring exactly the data a given API operation needs.
What problem do DTOs help avoid with lazy-loaded entity associations?
- Compilation errors
- Accidental serialization triggering lazy loads / LazyInitializationException
- Slow startup
- Missing annotations
Answer: Accidental serialization triggering lazy loads / LazyInitializationException. Serializing an entity can touch lazy associations, causing extra queries or LazyInitializationException; a DTO controls exactly what is exposed.
Why are Java records a natural fit for DTOs?
- They allow mutation everywhere
- They auto-connect to the database
- They replace controllers
- They are concise, immutable carriers of data
Answer: They are concise, immutable carriers of data. Records concisely declare immutable data carriers with generated accessors, equals, and toString, ideal for DTOs.
What is 'mapping' in this context?
- Drawing a UML diagram
- Converting between entities and DTOs
- Indexing a database
- Routing a URL
Answer: Converting between entities and DTOs. Mapping is converting data between entity objects and DTO objects in each direction.
What does a tool like MapStruct generate?
- Database tables
- REST endpoints
- Compile-time mapping code between types
- Validation rules
Answer: Compile-time mapping code between types. MapStruct generates type-safe mapping code at compile time, avoiding hand-written boilerplate and reflection.
Which is a good reason to keep separate request and response DTOs?
- They must always be identical
- Inbound and outbound shapes often differ (e.g. no id on create)
- Java forbids reuse
- It reduces classes
Answer: Inbound and outbound shapes often differ (e.g. no id on create). Request and response shapes commonly differ; for example a create request has no id while the response does.
Where should validation annotations typically live?
- Only on entities
- On the database
- Nowhere
- On request DTOs at the API boundary
Answer: On request DTOs at the API boundary. Putting validation on request DTOs validates input at the boundary without polluting persistence entities.
What is a downside of manual mapping compared to a mapping library?
- It is impossible
- More boilerplate to write and maintain
- It cannot compile
- It bypasses the JVM
Answer: More boilerplate to write and maintain. Manual mapping is explicit and dependency-free but means writing and maintaining repetitive conversion code.
Continue this course
- Previous: Exception Handling with @ControllerAdvice
- Next: Configuration & Properties — application.properties/yml, @Value and @ConfigurationProperties
- Quick reference: Java cheat sheet
Frequently asked questions
What is a DTO and how is it different from an entity?
A DTO (Data Transfer Object) is a simple object whose only job is to carry data across a boundary — typically between your API and its clients. An entity, by contrast, is mapped to a database table and represents persistent state, often with relationships, lazy associations, and internal fields. A DTO is shaped for a specific request or response and contains only the fields that operation needs. Keeping them separate lets your API and your database schema evolve independently.
Why is exposing JPA entities directly in the API considered a bad practice?
Several reasons. First, security: an entity may hold internal or sensitive fields — password hashes, audit columns, flags — that you do not want serialized into responses. Second, coupling: returning entities ties your public API contract to your database schema, so a column rename can break clients. Third, lazy loading: serializing an entity can accidentally trigger lazy associations, causing extra queries or a LazyInitializationException outside a transaction. A DTO gives you explicit, safe control over exactly what crosses the wire.
Why are Java records a good fit for DTOs?
Records are concise, immutable data carriers. A single line — record UserResponse(Long id, String name, String email) — declares the fields and generates a canonical constructor, accessor methods, equals, hashCode, and toString. DTOs are exactly that: immutable bundles of data with no behavior. Records remove the boilerplate of writing a class with private final fields, a constructor, and getters by hand, and their immutability is desirable for objects you just want to read after creation.
Should I write mapping code by hand or use a library like MapStruct?
Both are valid. Manual mapping — a small static method that copies fields between entity and DTO — is explicit, has zero dependencies, and is easy to read, which is fine for a handful of types. As the number of DTOs grows, hand-written mappers become repetitive boilerplate. MapStruct generates the mapping code at compile time from an annotated interface, giving you type-safe, fast conversions without reflection and without writing the copy logic yourself. Choose manual for small projects, a library when mappings proliferate.
Do I need separate DTOs for requests and responses?
Often, yes. Inbound and outbound shapes usually differ: a create request has no id (the server assigns it) and may include a raw password, while the response has an id and must never include the password. Reusing one class forces awkward nullable fields and mixes concerns. Separate request and response DTOs make each contract precise, let you put validation annotations only on request DTOs, and keep the API self-documenting about what is sent versus what is returned.