Se você trabalha com Spring Boot há algum tempo, provavelmente já escreveu centenas de classes DTO. No começo parece correto, organizado e seguro. Mas conforme o sistema cresce, você percebe que está mantendo duas representações do mesmo objeto — sem agregar valor real ao negócio.
Esse é um dos sinais clássicos de código com alto custo de manutenção.
DTOs não são o problema. O problema é escrevê-los manualmente.
Neste artigo, você vai aprender 5 padrões usados em projetos Spring Boot modernos para eliminar ou reduzir drasticamente boilerplate, melhorar a manutenibilidade e aumentar a produtividade.
O problema dos DTOs manuais
Considere uma entidade simples:
@Entity
public class Usuario {
@Id
private Long id;
private String nome;
private String email;
private String senha;
private LocalDateTime criadoEm;
}
DTO correspondente:
public class UsuarioDTO {
private Long id;
private String nome;
private String email;
}
Mapper manual:
public UsuarioDTO toDTO(Usuario usuario) {
UsuarioDTO dto = new UsuarioDTO();
dto.setId(usuario.getId());
dto.setNome(usuario.getNome());
dto.setEmail(usuario.getEmail());
return dto;
}
Problemas:
- Código repetitivo
- Alto custo de manutenção
- Risco de esquecer campos
- Duplicação estrutural
- Baixo valor agregado
Agora vamos aos padrões que resolvem isso.
Padrão 1 — Usar Records como DTO
Desde o Java 16, records são a melhor forma de definir DTOs.
Antes:
public class UsuarioDTO {
private Long id;
private String nome;
private String email;
}
Depois:
public record UsuarioDTO(
Long id,
String nome,
String email
) {}
Benefícios:
- Imutável por padrão
- Menos código
- Mais legível
- Seguro
Uso:
return new UsuarioDTO(
usuario.getId(),
usuario.getNome(),
usuario.getEmail()
);
Padrão 2 — Projection com Spring Data JPA
Este é um dos padrões mais poderosos e subutilizados.
Você pode retornar DTOs diretamente da query, sem mapper.
DTO projection:
public record UsuarioDTO(
Long id,
String nome,
String email
) {}
Repository:
@Repository
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
List<UsuarioDTO> findAllProjectedBy();
}
Spring faz o mapping automaticamente.
Benefícios:
- Zero mapper manual
- Melhor performance
- Evita carregar dados desnecessários
Padrão 3 — Constructor Projection (JPQL)
Permite retornar DTO diretamente via query.
DTO:
public record UsuarioDTO(
Long id,
String nome,
String email
) {}
Repository:
@Query("""
SELECT new com.exemplo.UsuarioDTO(
u.id,
u.nome,
u.email
)
FROM Usuario u
""")
List<UsuarioDTO> buscarUsuarios();
Benefícios:
- Sem mapper
- Controle total da query
- Performance otimizada
Padrão 4 — Interface-based Projection
Spring cria implementação automaticamente.
Projection:
public interface UsuarioView {
Long getId();
String getNome();
String getEmail();
}
Repository:
List<UsuarioView> findAllBy();
Uso:
UsuarioView usuario = repository.findById(id);
String nome = usuario.getNome();
Benefícios:
- Zero implementação
- Zero mapper
- Alta performance
Padrão 5 — MapStruct (quando mapping é necessário)
Quando você realmente precisa mapear objetos complexos, use MapStruct.
Dependência Maven:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
Mapper:
@Mapper(componentModel = "spring")
public interface UsuarioMapper {
UsuarioDTO toDTO(Usuario usuario);
}
Uso:
UsuarioDTO dto = mapper.toDTO(usuario);
Benefícios:
- Gera código em compile-time
- Zero reflection
- Alta performance
- Type-safe
Padrão bônus — Retornar DTO direto no Service
Evite retornar Entity em Controllers.
Errado:
@GetMapping
public Usuario getUsuario() {
return service.buscar();
}
Correto:
@GetMapping
public UsuarioDTO getUsuario() {
return service.buscar();
}
Service:
public UsuarioDTO buscar() {
return repository.buscarDTO();
}
Isso desacopla sua API do modelo interno.
Comparação dos padrões
| Padrão | Boilerplate | Performance | Facilidade |
|---|---|---|---|
| Record DTO | baixo | alta | alta |
| Projection | zero | altíssima | alta |
| Constructor projection | zero | altíssima | média |
| Interface projection | zero | altíssima | alta |
| MapStruct | baixo | altíssima | média |
Quando usar cada abordagem
Use Projection quando:
- Dados vêm do banco
- Não precisa lógica extra
Use Record quando:
- DTO simples
- API response
Use MapStruct quando:
- Mapping complexo
- Transformações
Use Interface Projection quando:
- Queries simples
- Alta performance
Arquitetura recomendada (produção)
Repository:
List<UsuarioDTO> findAllProjectedBy();
Service:
public List<UsuarioDTO> listar() {
return repository.findAllProjectedBy();
}
Controller:
@GetMapping
public List<UsuarioDTO> listar() {
return service.listar();
}
Sem mapper manual.
Sem boilerplate.
Resultado final
Você elimina:
- Centenas de classes desnecessárias
- Mappers manuais
- Código duplicado
E ganha:
- Código mais limpo
- Melhor performance
- Mais produtividade
- Arquitetura moderna
Conclusão
DTOs continuam sendo importantes, mas escrevê-los manualmente é um antipadrão em sistemas modernos.
Use:
- Records
- Spring Projections
- Constructor Projection
- Interface Projection
- MapStruct quando necessário
Esses padrões são utilizados em aplicações Spring Boot escaláveis e prontas para produção.





