Vídeos novos: segunda, quarta e sexta · 8h

DTO, Domínio e Entidade: quando usar cada um em Java

A diferença entre as camadas e como organizar um projeto de forma profissional.

DTO, Model e Entity

Em quase todo projeto Spring Boot aparece a mesma dúvida: “posso devolver a minha entidade direto no controller?”. Pode — mas cedo ou tarde isso cobra o seu preço. Para entender por quê, precisamos separar três conceitos que costumam se misturar: DTO, modelo de domínio e entidade.

Entidade: o espelho da tabela

A entidade representa como o dado é guardado. Com JPA, ela é anotada com @Entity e cada campo corresponde a uma coluna.

@Entity @Table(name = "clientes") public class ClienteEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nome; private String email; private String senhaHash; private LocalDateTime criadoEm; // getters e setters }

Ela conhece detalhes de infraestrutura: nomes de tabela, geração de ID, relacionamentos lazy. Isso é ótimo para o banco e péssimo para o resto da aplicação.

Domínio: as regras do negócio

O modelo de domínio representa o conceito do negócio e as regras que valem para ele, sem saber nada de banco ou de HTTP.

public class Cliente { private final Long id; private String nome; private Email email; public void alterarEmail(Email novo) { if (novo.equals(email)) { throw new RegraDeNegocioException("O e-mail é o mesmo"); } this.email = novo; } }

Em projetos pequenos é comum a entidade fazer também o papel de domínio — e tudo bem. Separar os dois compensa quando as regras crescem ou quando o modelo do banco e o modelo do negócio começam a divergir.

DTO: o contrato com quem está de fora

O Data Transfer Object define o formato dos dados que entram e saem da sua API. Em Java moderno, um record é perfeito para isso:

public record CriarClienteRequest( @NotBlank String nome, @Email String email, @Size(min = 8) String senha) {} public record ClienteResponse(Long id, String nome, String email) {}

Repare que o ClienteResponse não tem senhaHash. Se o controller devolvesse a entidade, esse campo iria parar no JSON. Esse é o primeiro grande motivo para usar DTOs.

Por que separar?

  • Segurança: você escolhe exatamente o que expõe e o que aceita (nada de alguém enviar "admin": true num cadastro).
  • Estabilidade da API: mudar uma coluna no banco não quebra os clientes da API.
  • Sem surpresas do JPA: serializar entidades com relacionamentos lazy gera LazyInitializationException ou loops infinitos no JSON.
  • Validação no lugar certo: as anotações de entrada ficam no DTO de request, não espalhadas pela entidade.

O fluxo completo

Request (DTO) ─▶ Controller ─▶ Service (domínio) ─▶ Repository (entidade) ─▶ Banco Response (DTO) ◀─ Controller ◀─ Service ◀─ Repository ◀─────────────────── Banco

A conversão pode ser feita à mão, num mapper simples:

@Component public class ClienteMapper { public ClienteResponse toResponse(ClienteEntity e) { return new ClienteResponse(e.getId(), e.getNome(), e.getEmail()); } } @PostMapping public ResponseEntity<ClienteResponse> criar(@RequestBody @Valid CriarClienteRequest req) { ClienteEntity salvo = service.criar(req); return ResponseEntity.status(HttpStatus.CREATED).body(mapper.toResponse(salvo)); }

Para projetos maiores, bibliotecas como o MapStruct geram esses mapeamentos em tempo de compilação.

Regra prática: a entidade nunca sai do service. O controller só conhece DTOs; o repository só conhece entidades; o service faz a ponte — e é lá que vivem as regras de negócio.