DTOs versus Entities keeps coming up in hallway chats right now. Java EE 5 just landed with JPA baked in, Hibernate keeps powering along, and Spring is everywhere in our projects. We are wiring services, exposing remote calls over EJB or SOAP, maybe flirting with REST, and we want clean, fast, simple code. The question is simple to ask and tricky to answer: where do we draw the service boundary, and what crosses it? Do we send JPA entities over the wire, or do we send Data Transfer Objects that are plain and boring and safe?
Definitions
JPA entity: a persistent class managed by an EntityManager. It carries identity, relationships, and usually lazy collections. It is often proxied by the provider and tied to a persistence context for change tracking.
DTO: a plain object with only data. No persistence vibes, no lazy loading, no proxies, usually serializable out of the box. It exists to cross a service boundary and to pin down a contract.
Service boundary: the line where a call becomes a remote call or a call that you treat as remote in your design. Think EJB remote, SOAP over HTTP, or a controller that marshals to JSON or XML for another process. Performance, versioning, and security concerns change right at that line.
Examples
Let us ground this with a tiny order domain. Here is a JPA entity that you would keep inside your service code.
import javax.persistence.*;
import java.util.*;
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue
private Long id;
private String number;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<OrderLine> lines = new ArrayList<>();
// getters and setters
}Here is a DTO shaped for a consumer. No lazy fields. Only what the client needs.
import java.io.Serializable;
import java.util.List;
public class OrderDto implements Serializable {
private Long id;
private String number;
private String customerName;
private List<LineDto> lines;
public static class LineDto implements Serializable {
public String sku;
public int qty;
}
// getters and setters
}And a tiny mapper. Keep it boring and testable.
public final class OrderMapper {
private OrderMapper() {}
public static OrderDto toDto(Order o) {
OrderDto dto = new OrderDto();
dto.setId(o.getId());
dto.setNumber(o.getNumber());
dto.setCustomerName(o.getCustomer().getName()); // force load in transaction
List<OrderDto.LineDto> lines = new ArrayList<>();
for (OrderLine l : o.getLines()) {
OrderDto.LineDto ld = new OrderDto.LineDto();
ld.sku = l.getSku();
ld.qty = l.getQty();
lines.add(ld);
}
dto.setLines(lines);
return dto;
}
}Now expose a service. The contract talks DTO. Your persistence and proxies stay inside.
import javax.jws.WebService;
import javax.ejb.Stateless;
import javax.persistence.*;
@WebService
@Stateless
public class OrderService {
@PersistenceContext EntityManager em;
public OrderDto findByNumber(String number) {
Order o = em.createQuery(
"select o from Order o join fetch o.customer where o.number = :n", Order.class)
.setParameter("n", number)
.getSingleResult();
// optionally fetch lines to avoid lazy problems
o.getLines().size();
return OrderMapper.toDto(o);
}
}Counterexamples
There are times when returning entities is fine. If your web tier runs in the same JVM and the call is truly local, you can return entities to a JSF or Spring MVC controller and render a page right away. The persistence context is still around inside the request, lazy loading works, and you avoid extra mapping code. This is common in monolithic web apps that do not cross a process boundary.
There are also times when sending entities blows up. The first call works in a test, then production throws a LazyInitializationException or a serialization error because a provider proxy slips into the payload. Or you expose sensitive fields by accident. Or a client updates an entity graph and you trigger unexpected writes on merge.
Decision rubric
- Is the boundary remote now or very likely soon? Use DTO.
- Do you need a stable contract that you can version without breaking clients? Use DTO.
- Do you need to trim the payload to avoid dragging a whole graph across the wire? Use DTO.
- Are you worried about lazy loading, proxies, or provider classes in your payload? Use DTO.
- Is the call local inside one request and you own both caller and callee? Returning entities is fine.
- Do you rely on automatic dirty checking across method calls in the same transaction? Returning entities inside the service is fine.
- Do you run batch jobs or internal modules where the code is in the same process and the team is the same? Entities are fine.
One more practical point. If you find yourself adding serialization to an entity or sprinkling eager fetch everywhere only to satisfy a remote client, you already crossed the line. That is DTO time.
Lesson learned
Keep entities inside the service boundary. Let them do what they do best inside a transaction. At the edge of your service, map to DTO and speak a clear contract. That keeps lazy loading honest, keeps payloads small, and keeps your API from leaking internal choices. If you are in a local only setup and you control both sides, return entities and move on. The day you add a remote client, add the DTO layer at the boundary and sleep better.
JPA gives us nice tools with annotations, queries, and clean programming. The trick is not to confuse a persistence model with an API model. Draw the boundary, be explicit, and write tiny mappers that are easy to test. Boring code pays rent.