Domain Driven Design (DDD) com Java e Spring é uma abordagem essencial para organizar aplicações complexas. Neste artigo, você vai aprender na prática como utilizar DTO, Model e Entity para estruturar seu sistema de forma escalável e desacoplada.
Sem uma arquitetura bem definida, o resultado costuma ser:
- regras de negócio espalhadas
- alto acoplamento
- dificuldade de manutenção
- baixa evolução do sistema
É nesse cenário que o Domain Driven Design (DDD) se destaca.
Ao utilizar Java 21 com Spring Boot, conseguimos estruturar aplicações onde o domínio é organizado e desacoplado das camadas externas.
💡 Dica: Este artigo possui um projeto completo no GitHub com todos os exemplos implementados (link ao longo do conteúdo).
📦 Projeto prático deste artigo
👉 https://github.com/isacaguiar/website-projects/tree/main/ddd-java-spring-dto-model-entitiy
O que você vai aprender
- Como estruturar aplicações com DDD e Spring
- Diferença entre DTO, Model e Entity
- Como mapear objetos com MapStruct
- Exemplo completo com Java 21
O problema de arquiteturas tradicionais
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:
❌ Entidade exposta na API
❌ Domínio inexistente
❌ Alto acoplamento
DDD propõe exatamente o oposto.
Arquitetura DDD com Java e Spring
Uma organização comum para projetos Java com Spring pode ser:
projeto
├─ application
│ └─ dto
│
├─ domain
│ └─ model
│
└─ 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 Model.
Infrastructure
Responsável pela comunicação com recursos externos:
-
banco de dados
-
APIs externas
-
mensageria
-
cache
Utiliza Entities.
Fluxo de dados no DDD com DTO, Model e Entity
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.
public record ClientDTO(Long id, String name, String documentNumber) {
}
public record AccountDTO(Long id, ClientDTO client, List items) {
public AccountDTO {
items = items == null ? List.of() : List.copyOf(items);
}
}
public record ItemBuyDTO(Long id, String description, BigDecimal value) {
}
DTOs devem ser simples e sem regras de negócio.
Models (Domain Layer)
Representa o modelo de domínio da aplicação.
Eles costumam ser imutáveis.
public record Client(Long id, String name, String docNumber) {
}
public record Account(Long id, Client client, List items) {
}
public record ItemBuy(Long id, String description, BigDecimal value) {
}
Entities (Infrastructure Layer)
Entities representam dados persistidos no banco.
@Getter
@Setter
@Entity
@NoArgsConstructor
public class ClientEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(nullable = false)
private Long id;
private String name;
private String docNumber;
}
@Getter
@Setter
@Entity
public class AccountEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(nullable = false)
private Long id;
@ManyToOne
private ClientEntity client;
private List items;
}
@Getter
@Setter
@Entity
public class ItemBuyEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(nullable = false)
private Long id;
private String description;
private BigDecimal value;
}
MapStruct no DDD com Java e Spring
Para converter objetos entre camadas, podemos utilizar MapStruct, que gera código automaticamente.
Exemplo:
@Mapper(componentModel = "spring")
public interface ClientMapper {
@Mapping(target = "docNumber", source = "documentNumber")
Client dtoToModel(ClientDTO dto);
ClientEntity modelToEntity(Client vo);
@Mapping(target = "documentNumber", source = "docNumber")
ClientDTO modelToDto(Client vo);
Client entityToModel(ClientEntity entity);
}
A anotação:
@Mapping(target = "docNumber", source = "documentNumber")
indica que:
DTO.documentNumber -> Model.docNumber
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.
@SpringJUnitConfig(classes = {
ClientMapperImpl.class
})
class ClientMapperTest {
@Autowired
private ClientMapper mapper;
@Test
void shouldConvertDtoToModel() {
ClientDTO dto = new ClientDTO(null, "Maria", "12345678900");
Client model = mapper.dtoToModel(dto);
assertAll(
() -> assertNull(model.id()),
() -> assertEquals("Maria", model.name()),
() -> assertEquals("12345678900", model.docNumber())
);
}
@Test
void shouldConvertModelToEntity() {
Client model = new Client(null, "Maria", "12345678900");
ClientEntity entity = mapper.modelToEntity(model);
assertAll(
() -> assertNull(entity.getId()),
() -> assertEquals("Maria", entity.getName()),
() -> assertEquals("12345678900", entity.getDocNumber())
);
}
@Test
void shouldConvertModelToDto() {
Client model = new Client(null, "Maria", "12345678900");
ClientDTO dto = mapper.modelToDto(model);
assertAll(
() -> assertNull(dto.id()),
() -> assertEquals("Maria", dto.name()),
() -> assertEquals("12345678900", dto.documentNumber())
);
}
@Test
void shouldConvertEntityToModel() {
ClientEntity entity = new ClientEntity();
entity.setName("Maria");
entity.setDocNumber("12345678900");
Client model = mapper.entityToModel(entity);
assertAll(
() -> assertNull(model.id()),
() -> assertEquals("Maria", model.name()),
() -> assertEquals("12345678900", model.docNumber())
);
}
@Test
void shouldReturnNullWhenDtoIsNull() {
assertNull(mapper.dtoToModel(null));
}
@Test
void shouldReturnNullWhenModelIsNull() {
assertNull(mapper.modelToEntity(null));
assertNull(mapper.modelToDto(null));
}
@Test
void shouldReturnNullWhenEntityIsNull() {
assertNull(mapper.entityToModel(null));
}
}
@SpringJUnitConfig(classes = {
AccountMapperImpl.class,
ClientMapperImpl.class
})
class AccountMapperTest {
@Autowired
private AccountMapper mapper;
@Test
void shouldConvertDtoToModel() {
ClientDTO clientDTO = new ClientDTO(1L, "Maria", "12345678900");
ItemBuyDTO itemBuyDTO_1 = new ItemBuyDTO(1L, "Produto Alfa", new BigDecimal("10.00"));
ItemBuyDTO itemBuyDTO_2 = new ItemBuyDTO(2L, "Produto Bravo", new BigDecimal("20.00"));
AccountDTO accountDTO = new AccountDTO(1L, clientDTO, List.of(itemBuyDTO_1, itemBuyDTO_2));
Account model = mapper.dtoToModel(accountDTO);
assertAll(
() -> assertEquals(1L, model.id()),
() -> assertEquals("Maria", model.client().name()),
() -> assertEquals("12345678900", model.client().docNumber()),
() -> assertEquals("Produto Alfa", model.items().get(0).description())
);
}
@Test
void shouldConvertModelToEntity() {
Client client = new Client(1L, "Maria", "12345678900");
ItemBuy itemBuy_1 = new ItemBuy(1L, "Produto Alfa", new BigDecimal("10.00"));
ItemBuy itemBuy_2 = new ItemBuy(2L, "Produto Bravo", new BigDecimal("20.00"));
Account account = new Account(null, client, List.of(itemBuy_1, itemBuy_2));
AccountEntity entity = mapper.modelToEntity(account);
assertAll(
() -> assertNull(entity.getId()),
() -> assertEquals(2, entity.getItems().size())
);
}
@Test
void shouldConvertModelToDto() {
Client client = new Client(1L, "Maria", "12345678900");
ItemBuy itemBuy_1 = new ItemBuy(1L, "Produto Alfa", new BigDecimal("10.00"));
ItemBuy itemBuy_2 = new ItemBuy(2L, "Produto Bravo", new BigDecimal("20.00"));
Account account = new Account(null, client, List.of(itemBuy_1, itemBuy_2));
AccountDTO dto = mapper.modelToDto(account);
assertAll(
() -> assertNull(dto.id()),
() -> assertEquals("Maria", dto.client().name()),
() -> assertEquals("12345678900", dto.client().documentNumber()),
() -> assertEquals(2, dto.items().size())
);
}
@Test
void shouldConvertEntityToModel() {
ClientEntity clientEntity = new ClientEntity();
clientEntity.setId(1L);
clientEntity.setName("Maria");
clientEntity.setDocNumber("12345678900");
ItemBuyEntity itemBuy_1 = new ItemBuyEntity();
itemBuy_1.setId(1L);
itemBuy_1.setDescription("Produto Alfa");
itemBuy_1.setValue(new BigDecimal("10.00"));
ItemBuyEntity itemBuy_2 = new ItemBuyEntity();
itemBuy_2.setId(2L);
itemBuy_2.setDescription("Produto Bravo");
itemBuy_2.setValue(new BigDecimal("20.00"));
AccountEntity entity = new AccountEntity();
entity.setId(1L);
entity.setClient(clientEntity);
entity.setItems(List.of(itemBuy_1, itemBuy_2));
Account model = mapper.entityToModel(entity);
assertAll(
() -> assertEquals(1L, model.id()),
() -> assertEquals("Maria", model.client().name()),
() -> assertEquals("12345678900", model.client().docNumber()),
() -> assertEquals(2, model.items().size())
);
}
@Test
void shouldReturnNullWhenDtoIsNull() {
assertNull(mapper.dtoToModel(null));
}
@Test
void shouldReturnNullWhenModelIsNull() {
assertNull(mapper.modelToEntity(null));
assertNull(mapper.modelToDto(null));
}
@Test
void shouldReturnNullWhenEntityIsNull() {
assertNull(mapper.entityToModel(null));
}
}
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
-
Model
-
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.
Entenda também Clean Architecture com Spring Boot
Share this content:
