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

25 min

Lesson 7 of 11

1d streak

The 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

task/TaskModels.java
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

A serializer walking lazy relationships can produce N+1 queries while a session is open or LazyInitializationException after it closes. It can also publish internal fields simply because a getter exists.

Keep mapping explicit and independently testable

task/TaskMapper.java
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

  1. 01Capture representative JSON fixtures or HTTP contract tests before changing a model.
  2. 02Rename an internal entity field and update only the mapper; the JSON property remains stable.
  3. 03Add optional response data additively when existing clients can ignore it.
  4. 04Use a new request or response type when create, update, and read semantics diverge.
  5. 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.

DtoMappingLab.java
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);
        }
    }
}
Never return JPA entities directly from REST endpoints to prevent accidental exposure of sensitive fields.
DTOs protect APIs from breaking changes when internal database schema evolves.
@JsonIgnoreProperties(ignoreUnknown = true) enforces the Tolerant Reader pattern for schema evolution.

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

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

Supplementary Module Resources