Pular para o conteúdo
Categoria: Fundamentos & Boas Práticas17 min de leitura

O que é DDD (Domain-Driven Design)?

Por Schematize Blog ·

Linguagem ubíqua, agregados e contextos delimitados: aprenda a modelar software a partir do domínio do negócio com Domain-Driven Design, seus padrões táticos e estratégicos, e quando (não) aplicá-los.

A maior dificuldade em construir software complexo raramente está na tecnologia: está em entender o problema do negócio e traduzi-lo fielmente para código. Domain-Driven Design (DDD) é uma abordagem que coloca o domínio do negócio no centro do design, alinhando o vocabulário do código ao vocabulário dos especialistas. Neste artigo você vai conhecer os conceitos centrais do DDD — linguagem ubíqua, agregados, contextos delimitados — entender como eles se encaixam, ver código concreto em Python e descobrir quando vale (e quando não vale) a pena aplicá-los.

O que é Domain-Driven Design?

Domain-Driven Design é uma abordagem de desenvolvimento de software apresentada por Eric Evans em seu livro de 2003 (Evans, 2003). A premissa central é que o coração do software está no domínio — a área de conhecimento e atividade em torno da qual o sistema gira — e que o design deve refletir profundamente esse domínio.

DDD parte de uma constatação: em sistemas complexos, o maior risco não é técnico, é a complexidade do domínio. Por isso, em vez de começar pelo banco de dados ou pela interface, o DDD começa por compreender e modelar o negócio em estreita colaboração com quem o conhece — os especialistas do domínio (domain experts).

Vale separar o que o DDD é do que ele não é. DDD não é um framework, não é uma estrutura de pastas e não é sinônimo de "usar repositórios". É, antes de tudo, uma disciplina de modelagem colaborativa: você conversa com quem entende o negócio, destila esse entendimento em um modelo e mantém esse modelo vivo no código. As ferramentas técnicas (entidades, agregados, repositórios) existem para sustentar o modelo, não o contrário.

É útil dividir o DDD em duas frentes:

    Um erro frequente de quem está começando é mergulhar direto no tático (porque é a parte com código) e ignorar o estratégico. Na prática, o estratégico costuma trazer mais retorno: errar as fronteiras de um sistema é muito mais caro do que escolher mal entre uma entidade e um value object.

    Domínio, subdomínio e núcleo

    Dentro de um mesmo negócio nem tudo tem a mesma importância. O DDD distingue:

      Reconhecer onde está o núcleo evita o desperdício de aplicar modelagem sofisticada em partes triviais e, ao mesmo tempo, garante atenção onde ela realmente importa.

      Linguagem ubíqua: o vocabulário compartilhado

      O conceito mais fundamental e transformador do DDD é a linguagem ubíqua (ubiquitous language). É um vocabulário comum, construído em conjunto por desenvolvedores e especialistas do domínio, usado em todos os lugares: nas conversas, na documentação e — crucialmente — no código.

      A ideia é eliminar a tradução. Se o especialista fala em "apólice", "sinistro" e "cobertura", essas mesmas palavras devem aparecer como classes, métodos e variáveis no código. Quando o desenvolvedor diz "registro" e o especialista diz "apólice" para a mesma coisa, há um espaço para mal-entendidos que vira bug.

      # Código sem linguagem ubíqua: genérico e desconectado do negócio
      def processar(self, item, valor):
          self.registros.append({"item": item, "valor": valor})
      
      # Código com linguagem ubíqua: fala a língua do domínio
      def registrar_sinistro(self, apolice: Apolice, valor: Dinheiro) -> Sinistro:
          sinistro = Sinistro.abrir(apolice, valor)
          self.sinistros.adicionar(sinistro)
          return sinistro

      Repare na diferença de leitura. O primeiro método não conta história nenhuma: "processar" o quê? Que "item"? Já o segundo é praticamente uma frase em português — qualquer pessoa do negócio consegue ler registrar_sinistro(apolice, valor) e confirmar se o comportamento está correto. Esse é o ganho prático da linguagem ubíqua: ela transforma o código em um documento revisável por quem não programa.

      A linguagem ubíqua não é definida de uma vez: ela é refinada continuamente à medida que o entendimento do domínio amadurece. Mudanças no vocabulário devem se refletir no código, e vice-versa. Quando um especialista corrige você — "isso não é um cancelamento, é uma rescisão" — essa correção precisa virar um rename no código, não apenas uma anotação esquecida em ata de reunião.

      Como construir a linguagem ubíqua na prática

      Algumas técnicas ajudam a destilar o vocabulário:

        Os blocos de construção do DDD tático

        O DDD tático oferece padrões para estruturar o modelo de domínio em código. Eles se encaixam muito bem dentro da camada de domínio de uma Arquitetura Limpa, onde as regras de negócio ficam isoladas de frameworks e infraestrutura.

        Entidades

        Entidades são objetos definidos por sua identidade, não por seus atributos. Um Cliente continua sendo o mesmo cliente mesmo que mude de nome ou endereço, porque tem um identificador único e contínuo no tempo.

        A consequência prática é que a igualdade de entidades é comparada por identidade, não por atributos. Dois clientes com o mesmo nome são pessoas diferentes; o mesmo cliente em dois momentos diferentes da vida é a mesma entidade.

        class Cliente:
            def __init__(self, id, nome: str):
                self._id = id
                self._nome = nome
        
            def renomear(self, novo_nome: str) -> None:
                if not novo_nome.strip():
                    raise ValueError("nome não pode ser vazio")
                self._nome = novo_nome
        
            def __eq__(self, outro) -> bool:
                return isinstance(outro, Cliente) and self._id == outro._id
        
            def __hash__(self) -> int:
                return hash(self._id)

        Value Objects (objetos de valor)

        Value objects são definidos por seus atributos, não por identidade. Dois objetos Dinheiro(100, "BRL") são intercambiáveis — não importa "qual" instância é qual. Eles costumam ser imutáveis e encapsulam validações e comportamentos relacionados ao valor.

        class Dinheiro:
            def __init__(self, quantia: int, moeda: str):
                if quantia < 0:
                    raise ValueError("quantia não pode ser negativa")
                self._quantia = quantia
                self._moeda = moeda
        
            def somar(self, outro: "Dinheiro") -> "Dinheiro":
                if self._moeda != outro._moeda:
                    raise ValueError("moedas diferentes")
                return Dinheiro(self._quantia + outro._quantia, self._moeda)

        Note duas decisões importantes nesse value object. Primeiro, ele é imutável: somar não altera o objeto, retorna um novo. Isso elimina toda uma classe de bugs por compartilhamento de estado. Segundo, ele protege suas invariantes no construtor: é impossível existir um Dinheiro com quantia negativa. Quando você modela valores assim, validações deixam de estar espalhadas pelo sistema e passam a viver em um único lugar.

        Value objects são também o melhor lugar para combater obsessão por primitivos (primitive obsession): em vez de passar str para e-mail, CPF e moeda por toda parte, crie Email, Cpf e Dinheiro. O tipo passa a carregar significado e garantias.

        Agregados e raízes de agregado

        Um agregado é um conjunto de objetos (entidades e value objects) tratado como uma unidade de consistência. Ele tem uma raiz de agregado (aggregate root), que é a única porta de entrada para o agregado: objetos externos só podem referenciar a raiz, nunca os objetos internos diretamente.

        Por exemplo, um Pedido (raiz) controla seus ItensDePedido. Você não adiciona um item manipulando a lista interna; você chama pedido.adicionar_item(...), e a raiz garante as invariantes (como "o total deve bater com a soma dos itens"). Isso protege a consistência das regras de negócio.

        class Pedido:  # raiz de agregado
            def __init__(self, id):
                self._id = id
                self._itens = []
        
            def adicionar_item(self, produto, quantidade):
                if quantidade <= 0:
                    raise ValueError("quantidade inválida")
                self._itens.append(ItemDePedido(produto, quantidade))
        
            @property
            def total(self) -> Dinheiro:
                return sum((item.subtotal for item in self._itens), Dinheiro(0, "BRL"))

        Duas regras de ouro sobre o desenho de agregados, defendidas por Vaughn Vernon (Vernon, 2013):

          A regra de consistência é direta: uma transação deve modificar um único agregado. Quando vários agregados precisam reagir a uma mudança, isso é feito por consistência eventual — tipicamente via eventos de domínio (veja a seguir), não dentro da mesma transação.

          Eventos de domínio

          Um evento de domínio representa algo relevante que aconteceu no domínio, no passado. Ele é nomeado no particípio ("Sinistro Aberto", "Pedido Confirmado") e carrega os dados necessários para que outras partes do sistema reajam. Eventos são a cola que permite agregados pequenos cooperarem sem acoplamento direto.

          from dataclasses import dataclass
          from datetime import datetime
          
          @dataclass(frozen=True)
          class PedidoConfirmado:
              pedido_id: str
              total: Dinheiro
              ocorrido_em: datetime

          A raiz de agregado registra o evento ao executar a operação, e a infraestrutura cuida de publicá-lo (por exemplo, em um message broker) depois que a transação é confirmada. Outros contextos — estoque, faturamento, notificação — assinam o evento e reagem por conta própria. Esse padrão é a base para integrar microsserviços sem acoplá-los no banco de dados.

          Fábricas (factories)

          Quando a criação de um agregado é complexa — envolve várias validações, monta value objects internos ou precisa garantir invariantes desde o nascimento —, essa lógica não deveria poluir o construtor nem ficar espalhada nos chamadores. Ela vira uma fábrica.

          class FabricaDeSinistro:
              @staticmethod
              def abrir(apolice: "Apolice", valor: Dinheiro, hoje: datetime) -> "Sinistro":
                  if not apolice.esta_vigente(hoje):
                      raise ValueError("apólice fora de vigência")
                  if valor.maior_que(apolice.limite_de_cobertura):
                      raise ValueError("valor excede a cobertura")
                  return Sinistro(id=novo_id(), apolice_id=apolice.id, valor=valor)

          A fábrica concentra a regra "como nasce um sinistro válido" em um único ponto, expressando a linguagem ubíqua (abrir) e garantindo que nunca exista um sinistro em estado inconsistente.

          Repositórios

          Repositórios fornecem a ilusão de uma coleção em memória de agregados, escondendo os detalhes de persistência. O domínio pede repositorio.buscar_por_id(...) sem saber se por trás há SQL, NoSQL ou um arquivo. Esse isolamento depende fortemente de injeção de dependência: o domínio define a interface do repositório, e a implementação concreta é fornecida de fora.

          from abc import ABC, abstractmethod
          
          class RepositorioDePedidos(ABC):  # interface no domínio
              @abstractmethod
              def buscar_por_id(self, pedido_id: str) -> "Pedido | None": ...
          
              @abstractmethod
              def salvar(self, pedido: "Pedido") -> None: ...

          A implementação concreta — RepositorioDePedidosPostgres, por exemplo — vive na camada de infraestrutura e implementa essa interface. Com isso, a lógica de domínio pode ser testada com um repositório em memória, sem banco de dados, e a tecnologia de persistência pode mudar sem tocar nas regras de negócio. Regra prática: há um repositório por agregado, e ele sempre devolve o agregado inteiro, em estado consistente — nunca pedaços soltos.

          Serviços de domínio

          Quando uma operação importante do domínio não pertence naturalmente a nenhuma entidade ou value object, ela vira um serviço de domínio — uma operação sem estado que expressa uma regra de negócio (por exemplo, uma transferência entre duas contas).

          A pergunta-chave é: "esse comportamento é de uma entidade?" Transferir dinheiro envolve duas contas; não cabe bem em nenhuma delas isoladamente. Por isso vira um serviço. O cuidado aqui é não exagerar: se quase tudo vira serviço de domínio, suas entidades provavelmente estão anêmicas (veja "Erros comuns").

          Dica: não confunda serviço de domínio com serviço de aplicação. O serviço de aplicação orquestra um caso de uso (abre transação, chama o repositório, dispara eventos) e não contém regra de negócio. O serviço de domínio contém regra de negócio, mas não conhece transações nem infraestrutura.

          Contextos delimitados: o coração do DDD estratégico

          Em um sistema grande, tentar criar um único modelo para tudo é uma receita para o caos: a palavra "cliente" significa coisas diferentes para o setor de vendas, de cobrança e de suporte. O DDD resolve isso com contextos delimitados (bounded contexts).

          Um contexto delimitado é uma fronteira explícita dentro da qual um modelo específico — e sua linguagem ubíqua — é coerente e válido. Dentro do contexto de "Vendas", "Cliente" tem um significado e um conjunto de atributos; dentro de "Cobrança", "Cliente" pode ser modelado de forma diferente. Cada contexto tem seu próprio modelo, e a tradução entre eles é explícita.

          Essa divisão é uma das pontes mais naturais entre DDD e microsserviços: um contexto delimitado costuma ser um ótimo candidato para virar um serviço independente, com seu próprio modelo, banco de dados e equipe. O alinhamento entre fronteiras de contexto e fronteiras de serviço evita o acoplamento que torna sistemas distribuídos frágeis.

          Atenção a uma armadilha clássica: contexto delimitado não é o mesmo que microsserviço. Um contexto pode conter mais de um serviço, e dividir microsserviços em fronteiras erradas (técnicas, e não de domínio) gera o pior dos mundos — um monólito distribuído, com todo o acoplamento de um monólito e toda a latência de uma rede.

          Mapa de contexto

          O mapa de contexto (context map) documenta como os diferentes contextos delimitados se relacionam: quem depende de quem, onde há tradução de modelos, quais são parceiros e quais são fornecedor/cliente. Ele dá uma visão estratégica das integrações do sistema.

          Alguns padrões de relacionamento que costumam aparecer no mapa:

            Camada anticorrupção (ACL)

            A camada anticorrupção merece destaque porque é uma das ferramentas mais úteis ao integrar com sistemas legados ou de terceiros. Em vez de deixar o modelo externo "vazar" para dentro do seu domínio, você cria uma camada de tradução que converte o vocabulário deles no seu.

            class GatewayDeCobranca:
                """Anti-corruption layer: traduz o modelo do ERP legado
                para a linguagem ubíqua do nosso domínio."""
            
                def __init__(self, cliente_erp):
                    self._erp = cliente_erp
            
                def emitir_fatura(self, pedido: "Pedido") -> "Fatura":
                    # o ERP fala "doc", "amt", "cur"; nós falamos Fatura/Dinheiro
                    resposta = self._erp.create_doc(
                        amount=pedido.total.quantia_em_centavos,
                        currency=pedido.total.moeda,
                    )
                    return Fatura(numero=resposta["doc_id"], valor=pedido.total)

            Sem a ACL, os termos estranhos do ERP (doc, amt, cur) acabariam infiltrados no seu código, corroendo a linguagem ubíqua que você lutou para construir.

            DDD, padrões e princípios de design

            Na implementação, o DDD se apoia em muitos design patterns essenciais: Repository para persistência, Factory para criação de agregados complexos, Strategy para variar regras de negócio, Observer/eventos de domínio para comunicar mudanças.

            Os blocos de construção também conversam diretamente com os princípios do SOLID. Agregados com responsabilidades bem definidas refletem o princípio da responsabilidade única; repositórios definidos como abstrações e implementados na infraestrutura aplicam a inversão de dependência. Martin Fowler, ao catalogar padrões de arquitetura corporativa, descreve o Domain Model como um modelo de objetos do domínio que incorpora tanto comportamento quanto dados (Fowler, 2002) — exatamente o oposto de um modelo anêmico, que só guarda dados sem comportamento.

            Vale também situar o DDD ao lado de duas abordagens que costumam aparecer junto dele. CQRS (separar o modelo de escrita do modelo de leitura) ajuda quando consultas complexas pressionam o modelo de domínio; Event Sourcing (persistir a sequência de eventos em vez do estado atual) combina naturalmente com eventos de domínio. Nenhum dos dois é obrigatório no DDD — são opções avançadas que só compensam quando o problema realmente as pede.

            Quando usar (e quando não usar) DDD

            O DDD tem custo: exige colaboração intensa com especialistas, modelagem cuidadosa e disciplina. Ele compensa quando:

              Ele tende a ser exagero quando:

                Nesses casos, aplicar todo o aparato tático do DDD adiciona complexidade sem retorno. Um software de cadastro simples raramente precisa de agregados elaborados — às vezes basta uma boa API e um modelo direto.

                Uma estratégia equilibrada para times que estão começando é adotar o DDD por partes. Use o DDD estratégico (linguagem ubíqua e contextos delimitados) em todo o sistema, porque é barato e quase sempre vale. Reserve o aparato tático completo (agregados, eventos, fábricas) para o núcleo — onde a complexidade está concentrada — e mantenha os subdomínios genéricos e de suporte simples, talvez com um CRUD honesto.

                Erros comuns

                  Perguntas frequentes

                  DDD é só para microsserviços? Não. DDD nasceu antes da onda de microsserviços e funciona perfeitamente em um monólito bem modelado — um monólito modular, com contextos delimitados como módulos internos, é uma excelente aplicação de DDD. Os contextos é que, mais tarde, podem virar serviços, se houver motivo.

                  DDD exige orientação a objetos? Não obrigatoriamente. Os conceitos mapeiam bem para OO, mas value objects imutáveis, eventos e funções puras também se expressam muito bem em estilo funcional. O essencial é o modelo e a linguagem, não o paradigma.

                  Qual a diferença entre entidade e value object, na prática? Pergunte: "se dois desses objetos tiverem os mesmos atributos, eles são a mesma coisa?" Se sim, é value object (dois Dinheiro(10, "BRL") são iguais). Se você precisa distingui-los e rastreá-los ao longo do tempo, é entidade (dois clientes homônimos são pessoas diferentes).

                  Posso adotar DDD aos poucos? Sim, e geralmente é o melhor caminho. Comece pela linguagem ubíqua e por identificar contextos. Introduza agregados e eventos só onde a complexidade justifica. Refatorar em direção ao DDD é uma jornada, não um big bang.

                  DDD e Clean Architecture são a mesma coisa? São complementares. A Arquitetura Limpa diz onde colocar as coisas (camadas, dependências apontando para dentro); o DDD diz o que colocar na camada de domínio e como modelá-la. Juntos formam uma combinação muito usada.

                  Conclusão

                  Domain-Driven Design é, antes de tudo, uma forma de levar o negócio a sério dentro do código. Com a linguagem ubíqua, desenvolvedores e especialistas falam a mesma língua; com agregados, value objects, fábricas e repositórios, o domínio ganha estrutura e consistência; com eventos de domínio, as partes cooperam sem se acoplar; e com contextos delimitados e mapas de contexto, sistemas grandes são divididos em partes coerentes e evolutivas. O DDD não é gratuito e não serve para todo projeto, mas em domínios complexos e duradouros ele é uma das abordagens mais eficazes para manter o software alinhado ao negócio ao longo do tempo. Comece pela linguagem ubíqua e pelos contextos, invista o aparato tático onde está o núcleo — o resto se constrói a partir daí.

                  Referências

                    Leituras relacionadas

                    Nenhum comentário ainda

                    Seja o primeiro a comentar.

                    Deixe seu comentário

                    Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

                    Entrar com Canverly