Pare de escrever DTOs boilerplate: 5 padrões Spring Boot que eliminam código repetitivo

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ãoBoilerplatePerformanceFacilidade
Record DTObaixoaltaalta
Projectionzeroaltíssimaalta
Constructor projectionzeroaltíssimamédia
Interface projectionzeroaltíssimaalta
MapStructbaixoaltíssimamé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.

Deixe um comentário