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:
-
como organizar uma arquitetura em camadas com DDD
-
a diferença entre DTO, Value Object e Entity
-
como mapear objetos usando MapStruct
-
exemplos completos em Java com Spring
Para facilitar o entendimento, utilizaremos um exemplo simples de um sistema de atendimento para bares e restaurantes.
Nosso sistema terá três conceitos principais:
-
Cliente
-
Conta
-
Item de compra
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:
-
Entidades de banco expostas na API
-
Regras de negócio espalhadas
-
Alto acoplamento
-
Dificuldade para evoluir o domínio
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:
-
APIs REST
-
Mensageria
-
integração com outros serviços
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:
-
banco de dados
-
APIs externas
-
mensageria
-
cache
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:
-
DTOs
-
Value Objects
-
Entities
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:
