Blog do Aguiar

Domain Driven Design com Java e Spring: DTO, Value Objects e Entities na prática

Aplicações corporativas modernas frequentemente lidam com domínios complexos, integrações externas, bancos de dados, mensageria e APIs.

Sem uma arquitetura clara, o resultado costuma ser um código difícil de manter:
controllers acessando diretamente entidades de banco, regras de negócio espalhadas e objetos sendo reutilizados em camadas inadequadas.

É nesse contexto que o Domain Driven Design (DDD) se torna uma abordagem poderosa.

Ao utilizar Java 21 com Spring, podemos estruturar o sistema de forma que o domínio de negócio fique isolado, enquanto as camadas externas apenas transportam dados.

Neste artigo veremos:

Para facilitar o entendimento, utilizaremos um exemplo simples de um sistema de atendimento para bares e restaurantes.

Nosso sistema terá três conceitos principais:

O problema de arquiteturas mal estruturadas

Em muitos projetos Java tradicionais vemos algo assim:

 


@RestController
public class ContaController {

    @Autowired
    ContaRepository repository;

    @PostMapping
    public ContaEntity criar(@RequestBody ContaEntity conta){
        return repository.save(conta);
    }
}

Esse tipo de abordagem gera alguns problemas sérios:

DDD propõe exatamente o oposto.

Arquitetura em camadas com DDD

Uma organização comum para projetos Java com Spring pode ser:

 

projeto
 ├─ application
 │   └─ dto
 │
 ├─ domain
 │   └─ vo
 │
 └─ infrastructure
     └─ entity

Application

Responsável pela entrada e saída de dados da aplicação.

Exemplos:

Utiliza DTOs.

Domain

Aqui vivem as regras de negócio.

Essa camada deve ser independente de frameworks.

Utiliza Value Objects.

Infrastructure

Responsável pela comunicação com recursos externos:

Utiliza Entities.

Fluxo de dados entre as camadas

O fluxo típico de dados pode ser representado assim:

 

Os objetos são convertidos entre si utilizando mappers.


Modelando o domínio do sistema

Nosso exemplo terá três objetos principais.

DTOs (Application Layer)

DTOs representam os dados de entrada e saída da aplicação.

package application.dto;

public class ClienteDTO {

    private String nome;
    private String identificador;

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public String getIdentificador() {
        return identificador;
    }

    public void setIdentificador(String identificador) {
        this.identificador = identificador;
    }
}
package application.dto;

import java.util.List;

public class ContaDTO {

    private Long id;
    private ClienteDTO cliente;
    private List itens;

}
package application.dto;

import java.math.BigDecimal;

public class ItemCompraDTO {

    private String descricao;
    private BigDecimal valor;

}

DTOs devem ser simples e sem regras de negócio.

Value Objects (Domain Layer)

Value Objects representam conceitos do domínio.

Eles costumam ser imutáveis.

package domain.vo;

public class ClienteVO {

    private final String nome;
    private final String cpf;

    public ClienteVO(String nome, String cpf) {
        this.nome = nome;
        this.cpf = cpf;
    }

    public String getNome() {
        return nome;
    }

    public String getCpf() {
        return cpf;
    }
}
package domain.vo;

import java.util.List;

public class ContaVO {

    private final Long id;
    private final ClienteVO cliente;
    private final List itens;

    public ContaVO(Long id, ClienteVO cliente, List itens) {
        this.id = id;
        this.cliente = cliente;
        this.itens = itens;
    }

}

Entities (Infrastructure Layer)

Entities representam dados persistidos no banco.

package infrastructure.entity;

import jakarta.persistence.*;

@Entity
public class ClienteEntity {

    @Id
    @GeneratedValue
    private Long id;

    private String nome;

    private String cpf;

}
@Entity
public class ContaEntity {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private ClienteEntity cliente;

}

Mapeamento entre objetos com MapStruct

Para converter objetos entre camadas, podemos utilizar MapStruct, que gera código automaticamente.

Exemplo:

@Mapper(componentModel = "spring")
public interface ClienteMapper {

    @Mapping(target = "cpf", source = "identificador")
    ClienteEntity toEntity(ClienteDTO dto);

}

A anotação:

@Mapping(target="cpf", source="identificador")

indica que:

DTO.identificador -> Entity.cpf

Quando os nomes dos atributos são iguais, não precisamos utilizar @Mapping.

Exemplo:

nome -> nome

MapStruct faz esse mapeamento automaticamente.

Testando o Mapper

Podemos validar o comportamento com testes unitários.

@SpringBootTest
class ClienteMapperTest {

    @Autowired
    ClienteMapper mapper;

    @Test
    void deveConverterDtoParaEntity(){

        ClienteDTO dto = new ClienteDTO();
        dto.setNome("Maria");
        dto.setIdentificador("12345678900");

        ClienteEntity entity = mapper.toEntity(dto);

        assertEquals("Maria", entity.getNome());
        assertEquals("12345678900", entity.getCpf());
    }
}

MapStruct vs ModelMapper

Uma dúvida comum é qual biblioteca utilizar para mapeamento.

Característica MapStruct ModelMapper
Geração de código Compile time Runtime
Performance Muito alta Média
Segurança de tipos Alta Média
Uso em projetos enterprise Muito comum Menos comum

Por gerar código em tempo de compilação, MapStruct costuma ser a escolha preferida em sistemas Spring Boot de alta escala.

Erros comuns ao usar DTO e Entity

Alguns erros muito comuns em projetos Java:

Usar Entity na API

@PostMapping
public ClienteEntity criar(@RequestBody ClienteEntity cliente)

Isso expõe o modelo de banco na API.

Regras de negócio em DTO

DTOs devem ser objetos de transporte, não de negócio.

Ignorar o domínio

Colocar toda a lógica em services ou controllers é um erro comum.

DDD coloca o domínio no centro da aplicação.

Benefícios dessa abordagem

Utilizar DTO, VO e Entity traz diversas vantagens:

✔ separação clara de responsabilidades
✔ domínio independente de frameworks
✔ código mais testável
✔ melhor manutenção
✔ maior escalabilidade

Conclusão

Domain Driven Design é uma abordagem poderosa para organizar sistemas complexos.

Ao utilizar Java com Spring, a separação entre:

permite manter o domínio protegido, melhorar a manutenção do código e facilitar a evolução da aplicação.

Com ferramentas como MapStruct, também conseguimos simplificar o mapeamento entre camadas e reduzir código repetitivo.

 

 

Share this content:

Sair da versão mobile