Entity vs DTO
An entity and a DTO can look almost identical — both are plain classes with fields and getters — yet they serve opposite purposes. An entity models a row in your database and is managed by the JPA persistence context; a DTO models the shape of data crossing an API boundary. Confusing the two is one of the most common sources of subtle bugs in Spring Boot apps.
Two objects, two responsibilities
An entity is owned by the persistence layer. It carries @Entity, an @Id, relationship mappings, and lives inside a Hibernate Session. A DTO is owned by the web layer. It carries validation annotations and Jackson hints, and is created fresh per request.
@Entity
@Table(name = "products")
public class Product {
@Id @GeneratedValue private Long id;
private String name;
private BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY)
private Category category; // a proxy until accessed
}
public record ProductResponse(
Long id, String name, BigDecimal price, String categoryName) {}
The entity has a lazy Category association; the DTO has a flat categoryName string. That difference is the whole point.
Side-by-side comparison
| Concern | Entity | DTO |
|---|---|---|
| Purpose | Persistence (DB row) | Data transfer (API contract) |
| Annotations | @Entity, @Id, @Column, @OneToMany | @NotBlank, @JsonProperty (optional) |
| Managed by | Hibernate persistence context | Nothing — just a POJO/record |
| Identity | Database primary key | Value-based / none |
| Lifecycle | Attached, detached, removed | Created and discarded per request |
| Relationships | Object graph with lazy proxies | Flattened / selected fields |
| Mutability | Mutable (dirty checking) | Often immutable (records) |
| Exposed to clients? | Should not be | Yes — that’s its job |
| Changes when… | The schema changes | The API contract changes |
See Entity mapping for the full set of JPA mapping annotations that live on the entity side.
Problems with returning entities directly
Returning an entity from a controller seems to save a mapping step, but it creates several failure modes.
LazyInitializationException
By the time Jackson serializes the response, the transaction has usually closed. Touching a LAZY association then has no Session to load it.
org.hibernate.LazyInitializationException:
could not initialize proxy [Category#7] - no Session
Workarounds like FetchType.EAGER or @Transactional on the controller just trade this bug for performance problems and an N+1 query storm.
Accidental over-exposure
Every field on the entity — including passwordHash, internal flags, and foreign keys — ships to the client unless you remember to suppress it. Security by “remembering to add @JsonIgnore” is fragile.
Mass-assignment on input
Accepting an entity as a @RequestBody lets a malicious client set fields they shouldn’t, such as role or id:
{ "name": "Widget", "price": 9.99, "role": "ADMIN" }
If role exists on the entity, Jackson binds it. A request DTO that simply has no role field closes the hole.
Tight coupling and recursion
Bidirectional relationships (Order ↔ LineItem) serialize into infinite recursion or require scattering @JsonManagedReference/@JsonBackReference across your domain model — polluting persistence code with serialization concerns.
Warning: “Just return the entity” works until your first lazy association, your first sensitive column, or your first schema rename. By then the API contract is already in production and hard to change.
When a DTO is overkill
DTOs are not free — they add classes and a mapping step. Skip them when the cost outweighs the benefit:
- Read-only DTO ≈ entity. For a tiny internal service where the entity is already flat, has no lazy associations, and exposes nothing sensitive, the DTO is pure boilerplate.
- Prototypes and spikes. When you’re validating an idea, not shipping a contract.
- Projections suffice. Spring Data can return interface- or record-based projections straight from a query — a lightweight DTO that avoids loading the full entity at all.
public interface ProductSummary {
String getName();
BigDecimal getPrice();
}
List<ProductSummary> findByCategoryId(Long categoryId);
Tip: A good default: always use DTOs for public APIs. For internal-only endpoints, use judgement — projections often hit the sweet spot.
Rule of thumb
If data leaves your application — across the network to a browser, a mobile app, or another service — give it a DTO. If it never leaves the persistence layer, an entity is fine. The boundary, not the convenience, decides.