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

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

Pro Tips

📋 Quick Reference

ConceptIdeaNote
entity@Entity, persistedPrivate storage
DTOdata transfer objectPublic contract
response DTOsafe outbound fieldsNo secrets
request DTOallowed inbound fieldsOften no id
record DTOrecord Foo(...)Immutable carrier
mappingentity <-> DTOBoth directions
manual mapperstatic toResponse(...)Explicit, no deps
MapStruct@Mapper interfaceGenerated code
validationon request DTOAt the boundary

📚 Resources / Keep Learning

🎉 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

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.

Related lessons