Skip to content
Spring Boot sb dto 3 min read

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

ConcernEntityDTO
PurposePersistence (DB row)Data transfer (API contract)
Annotations@Entity, @Id, @Column, @OneToMany@NotBlank, @JsonProperty (optional)
Managed byHibernate persistence contextNothing — just a POJO/record
IdentityDatabase primary keyValue-based / none
LifecycleAttached, detached, removedCreated and discarded per request
RelationshipsObject graph with lazy proxiesFlattened / selected fields
MutabilityMutable (dirty checking)Often immutable (records)
Exposed to clients?Should not beYes — that’s its job
Changes when…The schema changesThe 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.

Last updated June 13, 2026
Was this helpful?