DEV Community

André N. Darcie
André N. Darcie

Posted on

3 2 2 2 2

Entenda DDD de uma vez por todas, na implementação [PT-BR]

Introdução

O Domain-Driven Design (DDD), conforme definido no livro original Domain-Driven Design: Tackling Complexity in the Heart of Software, de Eric Evans, é uma abordagem para o desenvolvimento de software focada na modelagem do domínio central do negócio. Este post mostra uma implementação fiel aos conceitos do livro em um projeto de pagamentos usando C#.

O que o DDD é

  • ✅ Modelagem focada no domínio do negócio.
  • ✅ Comunicação clara entre desenvolvedores e especialistas do domínio.
  • ✅ Uso da Linguagem Onipresente (Ubiquitous Language).
  • ✅ Separação clara de responsabilidades.
  • ✅ Regras de negócio encapsuladas no domínio.
  • ✅ Limites explícitos entre diferentes contextos (Bounded Contexts).

O que o DDD não é

  • ❌ Não é necessário para sistemas simples.
  • ❌ Não é sobre focar em detalhes técnicos.
  • ❌ Não é apenas uma divisão em camadas.
  • ❌ Não é obrigatório usar repositórios ou serviços para tudo.
  • ❌ Não é uma metodologia rígida, mas uma abordagem de design.

1. Conceitos Fundamentais do DDD

  • Bounded Contexts: Define o limite explícito de cada parte do sistema, garantindo que cada contexto tenha seu próprio modelo e linguagem.
  • Entidades: Representam objetos com identidade única.
  • Value Objects: Representam conceitos do domínio sem identidade própria.
  • Agregados: Conjunto de entidades e objetos de valor que devem ser tratados como uma unidade.
  • Repositórios: Fornecem um acesso controlado aos agregados.
  • Serviços de Domínio: Contêm operações que não pertencem a uma única entidade.
  • Fábricas: Simplificam a criação de objetos complexos.

1.1 Linguagem Onipresente (Ubiquitous Language)

Antes de ir para a implementação, é essencial definir a Linguagem Onipresente para garantir uma comunicação clara entre desenvolvedores e especialistas do domínio. No sistema de pagamentos, que vamos implementar apresenta conceitos como Pedido de Pagamento (conjunto de pagamentos de um cliente), Pagamento (transferência de valor) e Método de Pagamento (forma de pagamento, como "Cartão de Crédito" ou "PIX") são usados diretamente nas classes PedidoDePagamento, Pagamento e MetodoPagamento.

Para isso, os desenvolvedores devem colaborar com equipes como o Comercial, para entender os tipos de transações e métodos de pagamento aceitos; o Financeiro, para garantir que as regras de cálculo e validação dos valores estejam corretas; e o Atendimento ao Cliente, para alinhar mensagens de erro claras, como "O valor do pagamento deve ser maior que zero". Essa colaboração garante que o sistema reflita a realidade do negócio.

1.2 DDD Não é Sobre Arquitetura, é Sobre o Domínio

Embora o DDD influencie a estrutura do código, ele não é sobre arquitetura, mas sim sobre compreender e modelar o domínio do negócio. Seu foco principal é capturar a lógica e os processos do domínio de forma clara e coesa, garantindo que o software reflita as necessidades da empresa. A arquitetura é apenas um meio de implementar esses conceitos, mas o verdadeiro valor do DDD está em alinhar a linguagem e o modelo do sistema com a realidade do negócio.

1.3 Mapa de Contexto (Context Map)

O Mapa de Contexto é uma ferramenta essencial no DDD, utilizada para visualizar como os diferentes Bounded Contexts interagem entre si. Ele define as relações, dependências e formas de comunicação entre os contextos, garantindo que o sistema mantenha a coesão e a integridade do domínio.

No sistema de pagamentos, por exemplo, o contexto de Pedidos de Pagamento poderia se relacionar com o contexto de Gestão Financeira para validar transações e gerar relatórios, enquanto o contexto de Atendimento ao Cliente acessa informações de pedidos para prestar suporte. Essas interações devem ser documentadas em um Mapa de Contexto, utilizando termos da Linguagem Onipresente, facilitando a comunicação entre equipes e garantindo que o design do sistema esteja alinhado às operações do negócio.

Para este exemplo, focaremos apenas no contexto de Pedidos de Pagamento para simplificar a implementação e destacar os principais conceitos do DDD sem a complexidade das interações entre múltiplos contextos. Essa abordagem facilita o entendimento inicial, permitindo aprofundar-se em mapas de contexto em cenários mais avançados.

1.4 Simplicidade e Clareza

Por fim, o DDD valoriza a simplicidade e a clareza do domínio, evitando abstrações desnecessárias e focando no que realmente importa para o negócio. No contexto de Pedidos de Pagamento, isso significa usar uma estrutura direta, com poucas camadas e apenas os componentes necessários para manter o domínio coeso. Repositórios, fábricas e serviços devem ser utilizados apenas quando agregam valor ao design.

2. Implementação

2.0 Entidade Pagamento

A classe Pagamento é uma entidade que possui um identificador único (Id), o qual garante sua distinção dentro do agregado. O construtor da entidade assegura a consistência do domínio ao exigir um valor positivo e um objeto válido do Value Object MetodoPagamento, evitando a criação de objetos em estado inválido. A propriedade Valor representa o montante da transação, enquanto a propriedade Metodo incorpora o Value Object que descreve o método de pagamento utilizado. Como parte do agregado PedidoDePagamento, a entidade Pagamento não pode existir fora do contexto desse agregado, garantindo que todas as operações relacionadas ao seu ciclo de vida sejam controladas pelo Aggregate Root.

public class Pagamento
{
    public Guid Id { get; private set; }
    public decimal Valor { get; private set; }
    public MetodoPagamento Metodo { get; private set; }

    internal Pagamento(decimal valor, MetodoPagamento metodo)
    {
        if (valor <= 0)
            throw new ArgumentException("O valor do pagamento deve ser maior que zero.");

        Id = Guid.NewGuid();
        Valor = valor;
        Metodo = metodo ?? throw new ArgumentNullException(nameof(metodo));
    }
}
Enter fullscreen mode Exit fullscreen mode

2.1 Entidade PedidoDePagamento

A classe PedidoDePagamento é um Aggregate Root que possui um identificador único (Id), controla a adição de objetos do tipo Pagamento através do método AdicionarPagamento, e expõe apenas uma coleção de leitura (IReadOnlyCollection), garantindo que os pagamentos só possam ser modificados dentro do próprio agregado, mantendo a integridade do domínio.

public class PedidoDePagamento
{
    public Guid Id { get; private set; }
    private readonly List<Pagamento> _pagamentos;
    public IReadOnlyCollection<Pagamento> Pagamentos => _pagamentos.AsReadOnly();

    internal PedidoDePagamento()
    {
        Id = Guid.NewGuid();
        _pagamentos = new List<Pagamento>();
    }

    internal void AdicionarPagamento(decimal valor, MetodoPagamento metodo)
    {
        var pagamento = new Pagamento(valor, metodo);
        _pagamentos.Add(pagamento);
    }
}
Enter fullscreen mode Exit fullscreen mode

2.2 Value Object MetodoPagamento

A classe MetodoPagamento é um Value Object, não possui um identificador único, sendo caracterizada pelo valor da propriedade Nome, cuja imutabilidade é garantida pelo modificador somente leitura (get). Sua consistência é assegurada pelo construtor, que impede a criação de objetos com valores nulos ou em branco, garantindo que o objeto esteja sempre em um estado válido. Além disso, a classe existe apenas para descrever o meio de pagamento dentro de um contexto maior, como o agregado PedidoDePagamento, evidenciando que seu valor é mais relevante do que sua identidade.

public sealed class MetodoPagamento
{
    public string Nome { get; }

    public MetodoPagamento(string nome)
    {
        Nome = string.IsNullOrWhiteSpace(nome) 
            ? throw new ArgumentNullException(nameof(nome), "Método de pagamento não pode ser nulo ou vazio.")
            : nome;
    }
}
Enter fullscreen mode Exit fullscreen mode

2.3 Fábrica PedidoDePagamentoFactory

Obs: Segundo o DDD, as fábricas são necessárias apenas para a criação de objetos complexos. Como PedidoDePagamento não é tão complexo, a fábrica pode ser dispensada, adicionei apenas como exemplo.

A classe PedidoDePagamentoFactory é uma Factory expõe o método estático CriarPedidoDePagamento, que simplifica o processo de criação de um PedidoDePagamento ao inicializar a entidade com um pagamento já adicionado. Esse método garante que o objeto seja criado em um estado válido, utilizando a classe MetodoPagamento para validar o nome do método de pagamento, promovendo a coesão e ocultando detalhes de implementação do processo de construção.

public static class PedidoDePagamentoFactory
{
    public static PedidoDePagamento CriarPedidoDePagamento(decimal valor, string metodo)
    {
        var pedido = new PedidoDePagamento();
        pedido.AdicionarPagamento(valor, new MetodoPagamento(metodo));
        return pedido;
    }
}
Enter fullscreen mode Exit fullscreen mode

2.4 Repositório IPedidoDePagamentoRepository

O Repositório é um padrão utilizado para abstrair o acesso aos dados, fornecendo uma interface que permite recuperar e armazenar objetos de domínio sem expor os detalhes da infraestrutura. A interface IPedidoDePagamentoRepository é um repositório que define o contrato para a persistência da entidade PedidoDePagamento, garantindo que o domínio permaneça independente da tecnologia de armazenamento. O método Adicionar permite incluir um novo pedido no repositório, enquanto o método ObterPorId possibilita a recuperação de um pedido existente a partir do seu identificador único (Guid), assegurando o encapsulamento do acesso aos dados e mantendo a coesão do domínio.

public interface IPedidoDePagamentoRepository
{
    void Adicionar(PedidoDePagamento pedido);
    PedidoDePagamento ObterPorId(Guid id);
}
Enter fullscreen mode Exit fullscreen mode

2.5 Serviço de Domínio ProcessadorDePedido

Obs: segundo o DDD, o serviço de domínio deve conter lógica que não pode ser atribuída a nenhuma entidade. Se o ProcessadorDePedido apenas delega para a fábrica e repositório, pode ser desnecessário, adicionei apenas como parte do exemplo, assim como a Factory.

A classe ProcessadorDePedido é um serviço de domínio que coordena o processo de criação e persistência de um PedidoDePagamento, sem violar o princípio de responsabilidade única. Ela recebe uma instância de IPedidoDePagamentoRepository via injeção de dependência, promovendo a desacoplagem entre o domínio e a infraestrutura de dados. O método Processar utiliza a Factory PedidoDePagamentoFactory para garantir a criação consistente do pedido, e em seguida delega ao repositório a responsabilidade de armazená-lo.

public class ProcessadorDePedido
{
    private readonly IPedidoDePagamentoRepository _repository;

    public ProcessadorDePedido(IPedidoDePagamentoRepository repository)
    {
        _repository = repository;
    }

    public void Processar(decimal valor, string metodo)
    {
        var pedido = PedidoDePagamentoFactory.CriarPedidoDePagamento(valor, metodo);
        _repository.Adicionar(pedido);
    }
}
Enter fullscreen mode Exit fullscreen mode

2.6 DbContext PagamentosDbContext

A classe PagamentosDbContext define os conjuntos de dados para as entidades PedidoDePagamento e Pagamento, permitindo operações de leitura e gravação no banco. O método OnModelCreating garante o mapeamento correto das entidades, definindo as chaves primárias com o método HasKey, assegurando a integridade dos dados no nível do banco. O Value Object MetodoPagamento é configurado como um Owned Type, de acordo com os princípios do DDD, garantindo que ele não possua uma tabela própria, mas seja armazenado junto à entidade Pagamento. Além disso, o construtor recebe as opções de configuração necessárias, promovendo a flexibilidade e a integração com diferentes provedores de banco de dados, enquanto mantém o domínio desacoplado da infraestrutura.

public class PagamentosDbContext : DbContext
{
    public PagamentosDbContext(DbContextOptions<PagamentosDbContext> options) : base(options) {}

    public DbSet<PedidoDePagamento> PedidosDePagamento { get; set; }
    public DbSet<Pagamento> Pagamentos { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PedidoDePagamento>().HasKey(p => p.Id);
        modelBuilder.Entity<Pagamento>().HasKey(p => p.Id);

        modelBuilder.Entity<Pagamento>().OwnsOne(p => p.Metodo, metodo =>
        {
            metodo.Property(m => m.Nome)
                .IsRequired()
                .HasMaxLength(100)
                .HasColumnName("MetodoPagamentoNome");
        });

        base.OnModelCreating(modelBuilder);
    }
}
Enter fullscreen mode Exit fullscreen mode

2.7 Repositório Concreto PedidoDePagamentoRepository

A classe PedidoDePagamentoRepository é uma implementação concreta da interface IPedidoDePagamentoRepository, utilizando o PagamentosDbContext para realizar as operações de acesso ao banco de dados. O método Adicionar insere uma nova instância de PedidoDePagamento no contexto e salva as alterações no banco de dados através do método SaveChanges, garantindo a persistência imediata. O método ObterPorId busca uma instância de PedidoDePagamento com base no identificador único (Guid), utilizando o método Include para carregar a coleção de pagamentos associados, assegurando que o agregado seja recuperado em sua totalidade, conforme o princípio da consistência transacional do DDD. Dessa forma, o repositório mantém a integridade do domínio ao isolar os detalhes da persistência, promovendo um código mais limpo e coeso.

public class PedidoDePagamentoRepository : IPedidoDePagamentoRepository
{
    private readonly PagamentosDbContext _context;

    public PedidoDePagamentoRepository(PagamentosDbContext context)
    {
        _context = context;
    }

    public void Adicionar(PedidoDePagamento pedido)
    {
        _context.PedidosDePagamento.Add(pedido);
        _context.SaveChanges();
    }

    public PedidoDePagamento ObterPorId(Guid id)
    {
        return _context.PedidosDePagamento.Include(p => p.Pagamentos).FirstOrDefault(p => p.Id == id);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Estutura final da implementação

A camada Domain representa o núcleo do negócio, contendo as entidades Pagamento e PedidoDePagamento, o Value Object MetodoPagamento, a interface do repositório IPedidoDePagamentoRepository, o serviço de domínio ProcessadorDePedido e a fábrica PedidoDePagamentoFactory para a criação consistente dos agregados. Já a camada Infrastructure implementa os detalhes técnicos de persistência dos dados usando o EF Core, com o contexto PagamentosDbContext para configurar o banco de dados e o repositório concreto PedidoDePagamentoRepository, que realiza a interação com o banco seguindo o contrato definido na camada de domínio. Essa organização garante um sistema modular, coeso e de fácil manutenção, com o domínio livre de dependências externas.

PagamentoProjeto
│
├── PagamentoContext
│   ├── Domain
│   │   ├── Aggregates
│   │   │   ├── PedidoDePagamento
│   │   │   │   ├── PedidoDePagamento.cs
│   │   │   │   ├── Pagamento.cs
│   │   │   │   └── ValueObjects
│   │   │   │       └── MetodoPagamento.cs
│   │   ├── Factories
│   │   │   └── PedidoDePagamentoFactory.cs
│   │   ├── Repositories
│   │   │   └── IPedidoDePagamentoRepository.cs
│   │   └── Services
│   │       └── ProcessadorDePedido.cs
│   │
│   └── Infrastructure
│       ├── Data
│       │   ├── Context
│       │   │   └── PagamentosDbContext.cs
│       │   └── Repositories
│       │       └── PedidoDePagamentoRepository.cs
Enter fullscreen mode Exit fullscreen mode

3. O Que Não Foi Utilizado do DDD Nesta Implementação

Essa implementação optou por não utilizar alguns elementos que, embora recomendados, não eram essenciais para o exemplo apresentado. Não foram aplicados os conceitos de Domínio Rico (Rich Domain) em sua plenitude, nem o uso extensivo de Domain Events, que são úteis para notificar outras partes do sistema sobre mudanças no domínio. A camada de Application Services, que poderia coordenar a interação entre o domínio e a interface do usuário, também foi omitida para manter a simplicidade. Além disso, não foi abordado o uso de Anti-Corruption Layers para integração com sistemas externos, nem o padrão Specification, que ajuda a encapsular regras de negócio reutilizáveis. Essas ausências, no entanto, não comprometem o entendimento dos fundamentos do DDD, pois o foco do post foi demonstrar a aplicação prática dos elementos mais essenciais da abordagem.

4. Conclusão

Em suma, esta implementação exemplifica de forma clara e prática como aplicar os princípios do Domain-Driven Design (DDD) conforme definidos no livro original de Eric Evans. A organização modular do código, com entidades, Value Objects, agregados, repositórios e serviços de domínio, demonstra como esses componentes colaboram para manter a integridade do domínio e a coesão do sistema. Além disso, a separação entre as camadas de domínio e infraestrutura garante um design desacoplado, facilitando a manutenção e evolução do projeto. Dessa forma, a abordagem apresentada mostra como o DDD pode contribuir para o desenvolvimento de sistemas mais robustos, compreensíveis e alinhados às necessidades do negócio.

Image of Datadog

The Future of AI, LLMs, and Observability on Google Cloud

Datadog sat down with Google’s Director of AI to discuss the current and future states of AI, ML, and LLMs on Google Cloud. Discover 7 key insights for technical leaders, covering everything from upskilling teams to observability best practices

Learn More

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay