Blog do Aguiar

Domain Driven Design com Java e Spring: DTO, Model e Entity na prática

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:

É 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

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:

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:

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:

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:

Sair da versão mobile