Design tático do DDD
Conforme descrito na publicação “Design estratégico do DDD”, o DDD pode ser dividido em duas partes: estratégico (“o quê e por quê estamos construindo”) e tático.
O design tático trata de como implementar o modelo de domínio dentro de um contexto delimitado. O objetivo é transformar o conhecimento levantado no design estratégico em código que represente a lógica de negócio preservando consistência, expressividade e manutenibilidade.
Seguindo a sequência, neste artigo abordo os padrões e conceitos do design tático no DDD. O artigo é baseado no livro “Aprendendo Domain-Driven Design” de Vlad Khononov.
Press enter or click to view image in full size

Implementando uma Lógica de Negócio Simples
A lógica de negócio é o núcleo do software. Ela representa as regras, decisões e invariantes que definem porque o sistema existe. Existem diferentes formas de implementar a lógica conforme a complexidade do domínio.
Script de Transação
O padrão Script de Transação organiza a lógica de negócio de forma procedural. Cada procedimento implementa uma operação específica do sistema, exposta por meio de uma interface pública.
É o padrão mais comum utilizado na maioria dos sistemas, essas operações públicas atuam como limites de encapsulamento, cada uma coordena validações, acessos a dados e atualizações necessárias para executar uma ação do negócio.
Características principais:
- Cada operação deve ser atômica, ou é concluída com sucesso ou falha completamente.
- O fluxo é explícito e sequencial, facilitando o entendimento em domínios simples.
- A consistência é garantida dentro da própria transação, geralmente alinhada a uma única operação do sistema.
Esse padrão é adequado quando a complexidade do domínio é baixa e as regras de negócio são diretas.
class OrderPaymentService {
public void pay(Order order) {
if (!”ABERTO”.equals(order.status)) {
throw new IllegalStateException(“Pedido não pode ser pago”);
}
order.status = “PAGO”;
System.out.println(“Pedido “ + order.id + “ pago com sucesso”);
}
}
- Não existe comportamento no objeto Order
- O objeto apenas carrega dados
- O serviço manda no fluxo
- A regra está explícita e centralizada
- Não há polimorfismo, invariantes distribuídas ou agregados
Registro Ativo
O padrão Registro Ativo junta dados e comportamento em um único objeto que representa uma tabela ou registro do banco de dados.
Cada instância encapsula tanto o estado quanto operações de salvar, atualizar, remover, regras simples de negócio, etc. É uma abordagem prática para domínios simples, mas pode se tornar difícil de manter conforme aumenta a complexidade da lógica de negócio.
class Order {
Long id;
String status;
Order(Long id, String status) {
this.id = id;
this.status = status;
}
public void pay() {
if (!”ABERTO”.equals(status)) {
throw new IllegalStateException(“Pedido não pode ser pago”);
}
status = “PAGO”;
save();
}
public void save() {
System.out.println(“Salvando pedido “ + id + “ com status “ + status);
}
public void delete() {
System.out.println(“Removendo pedido “ + id);
}
}
- O objeto Order representa diretamente a tabela Pedido
- Ele contém os dados
- Ele contém a regra de negócio
- Ele sabe se salvar e se remover
- Não existe serviço de aplicação
Lidando com Lógica de Negócio Complexa
Quando o domínio tem um nível mais alto de complexidade, padrões procedurais deixam de ser suficientes. Nesse cenário é onde entra o Modelo de Domínio.
Modelo de Domínio (Domain model)
O Modelo de Domínio é um modelo orientado a objetos que encapsula dados e comportamentos do domínio de negócio. Ele é composto pelos principais padrões táticos do DDD:
- Agregados
- Entidades
- Objetos de Valor
- Eventos de Domínio
- Serviços de Domínio
Ele deve ser o mais expressivo possível para capturar regras, invariantes e comportamentos essenciais do negócio. Isso sem se preocupar com características técnicas como persistência, mensageria ou infraestrutura.
Ao focar na lógica de negócio, o modelo passa a refletir diretamente a Linguagem Ubíqua do contexto delimitado, tornando o código mais fácil, expressivo e alinhado com o entendimento dos especialistas do domínio.
Entidades
Entidades representam conceitos do domínio que possuem identidade própria e ciclo de vida. Duas entidades podem ter os mesmos atributos, mas ainda serem diferentes por conta de sua identidade.
Características principais:
- São identificadas por um identificador único.
- Seus atributos podem mudar ao longo do tempo sem que a identidade seja alterada.
- Encapsulam comportamento e regras relacionadas ao seu ciclo de vida.
Enquanto objetos de valor representam descrições imutáveis, entidades representam coisas que evoluem no domínio. A diferença entre entidades e objetos de valor é importante para evitar modelos complexos ou inconsistentes.
class Order {
private final UUID id;
private OrderStatus status;
private LocalDateTime paidAt;
public Order(UUID id) {
this.id = id;
this.status = OrderStatus.CREATED;
}
/_ IDENTIDADE _/
public UUID getId() {
return id;
}
/_ COMPORTAMENTO DE DOMÍNIO _/
public void pay() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException(
“Pedido só pode ser pago se estiver no estado CREATED”
);
}
this.status = OrderStatus.PAID;
this.paidAt = LocalDateTime.now();
}
public void cancel() {
if (status == OrderStatus.PAID) {
throw new IllegalStateException(
“Pedido pago não pode ser cancelado”
);
}
this.status = OrderStatus.CANCELLED;
}
/_ CONSULTAS DE ESTADO (SEM REGRA) _/
public boolean isPaid() {
return status == OrderStatus.PAID;
}
}
- O id define a identidade, não os atributos
- Dois pedidos podem ter os mesmos dados, mas não são o mesmo pedido
- O estado muda ao longo do tempo
- As regras estão dentro da entidade
- O estado só muda por meio de métodos explícitos
Objeto de Valor
Ao contrário da entidade, objetos de valor representam conceitos do domínio que não possuem identidade, sendo definidos por seus atributos.
O uso excessivo de tipos primitivos (strings, inteiros, decimais) para representar conceitos do domínio é conhecido como Primitive Obsession. Objetos de valor resolvem esse problema ao encapsular significado, validações e comportamento em tipos explícitos do domínio.
Características principais:
- São imutáveis: qualquer alteração resulta em uma nova instância.
- São comparados por valor, não por identidade.
- Ajudam a expressar regras de negócio de forma clara e segura.
public final class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException(“Valor monetário inválido”);
}
if (currency == null || currency.isBlank()) {
throw new IllegalArgumentException(“Moeda inválida”);
}
this.amount = amount;
this.currency = currency;
}
/_ COMPORTAMENTO DE DOMÍNIO _/
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException(“Moedas diferentes”);
}
return new Money(
this.amount.add(other.amount),
this.currency
);
}
/_ IGUALDADE POR VALOR _/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 &&
currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount.stripTrailingZeros(), currency);
}
}
- Não existe id
- Dois Money(100, “BRL”) são a mesma coisa
- Não faz sentido “alterar” um Money
- Toda mudança gera uma nova instância
- As regras vivem dentro do tipo, não espalhadas pelo sistema
Agregados
Um Agregado é um cluster de entidades e objetos de valor tratado como uma única unidade de consistência. Ele define uma fronteira entre o que está dentro do agregado e o mundo externo.
Principais conceitos:
- O agregado é a fronteira de consistência: todas as invariantes devem ser garantidas dentro de seus limites.
- Apenas a raiz do agregado pode ser acessada diretamente por outros objetos.
- Operações de modificação devem acontecer por meio de métodos públicos da raiz do agregado, muitas vezes recebendo comandos que representam intenções do negócio.
Outros aspectos relevantes:
- O agregado define o limite transacional: uma transação não deve atravessar múltiplos agregados.
- Um agregado pode conter uma hierarquia de entidades, mas nem toda entidade é um agregado.
- Entidades e objetos de valor coexistem dentro do agregado, cada um com responsabilidades bem definidas.
Agregados podem notificar eventos de domínio para sinalizar mudanças no estado do negócio. Esses eventos reforçam a atomicidade das operações e facilitam integrações e reações assíncronas.
A modelagem de agregados deve refletir diretamente a Linguagem Ubíqua, utilizando termos e comportamentos reconhecíveis pelos especialistas do domínio.
/**
- AGREGADO
- Raiz do agregado: Order
- Fronteira de consistência: Pedido e seus itens
/
class Order {
/ IDENTIDADE DA RAIZ DO AGREGADO */
private final UUID id;
/_ ESTADO DO AGREGADO _/
private OrderStatus status;
private final List
private Money total;
public Order(UUID id) {
this.id = id;
this.status = OrderStatus.CREATED;
this.items = new ArrayList<>();
this.total = Money.zero();
}
/_ RAIZ DO AGREGADO _/
public UUID getId() {
return id;
}
public OrderStatus getStatus() {
return status;
}
/**
- Operação de negócio exposta pelo agregado.
- Garante as invariantes internas.
*/
public void addItem(ProductId productId, int quantity, Money unitPrice) {
ensureOrderIsOpen();
ensureValidQuantity(quantity); OrderItem item = new OrderItem(productId, quantity, unitPrice);
items.add(item); recalculateTotal();
}
public void confirm() {
ensureOrderIsOpen();
if (items.isEmpty()) {
throw new IllegalStateException(“Pedido não pode ser confirmado sem itens”);
}
this.status = OrderStatus.CONFIRMED;
}
/_ INVARIANTES DO AGREGADO _/
private void ensureOrderIsOpen() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException(
“Pedido confirmado não pode ser alterado”
);
}
}
private void ensureValidQuantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException(“Quantidade inválida”);
}
}
private void recalculateTotal() {
this.total = items.stream()
.map(OrderItem::subtotal)
.reduce(Money.zero(), Money::add);
}
/_ CONSULTAS (SEM MODIFICAÇÃO) _/
public List
return Collections.unmodifiableList(items);
}
public Money getTotal() {
return total;
}
}
- Order é a raiz do agregado
- Tudo passa por Order
- OrderItem não pode ser modificado externamente
- As invariantes estão centralizadas
- O agregado define o limite transacional
- Objetos de valor (Money, ProductId) reforçam o modelo
Eventos de Domínio
Eventos de Domínio representam fatos que ocorreram no domínio de negócio. Eles descrevem mudanças de estado nos agregados e fazem parte da essência do modelo, servindo para muito mais do que apenas integrar sistemas tecnicamente.
Características principais:
- São imutáveis.
- Expressam algo que aconteceu no passado.
- Utilizam a terminologia da Linguagem Ubíqua.
Eventos de domínio permitem:
- Comunicação desacoplada entre partes do sistema.
- Integração entre contextos delimitados.
- Implementação de fluxos reativos e assíncronos.
Eles também reforçam a atomicidade das operações do agregado, um evento só é publicado se a mudança de estado for concluída com sucesso.
/**
- EVENTO DE DOMÍNIO
- — Representa um fato que já aconteceu
- — É imutável
- — Usa linguagem do domínio
*/
public final class OrderConfirmed {
private final UUID orderId;
private final Instant occurredAt;
public OrderConfirmed(UUID orderId, Instant occurredAt) {
this.orderId = orderId;
this.occurredAt = occurredAt;
}
public UUID getOrderId() {
return orderId;
}
public Instant getOccurredAt() {
return occurredAt;
}
}
/**
Emissão do Evento pelo Agregado
*/
public class Order {
private final UUID id;
private OrderStatus status; private final List// Evento só é criado após mudança de estado bem-sucedida domainEvents.add( new OrderConfirmed(this.id, java.time.Instant.now()) );} public List
- O evento não comanda, ele relata
- Ele diz Pedido foi confirmado, não Confirme o pedido
- Ele nasce depois da regra ser aplicada
- Ele é imutável
- Ele não contém lógica de negócio
Serviços de Domínio
Serviços de Domínio encapsulam lógica de negócio que:
- Não pertence naturalmente a uma entidade ou objeto de valor.
- Envolve múltiplos agregados ou conceitos do domínio.
Eles são sem estado e focados em comportamento. Exemplos:
- Orquestração de operações entre agregados.
- Cálculos complexos que envolvem múltiplos conceitos do domínio.
- Regras que não se encaixam claramente em um único objeto.
Serviços de domínio devem utilizar a Linguagem Ubíqua e permanecer livres de responsabilidades técnicas.
/**
- SERVIÇO DE DOMÍNIO
- — Sem estado
- — Regra de negócio pura
- — Envolve múltiplas entidades
*/
public class TransferService {
public void transfer(Account source, Account target, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException(“Valor inválido”);
}
source.debit(amount);
target.credit(amount);
}
}
“Se a regra é do negócio, mas não é de ninguém em específico, então ela pertence a um serviço de domínio”
Por que isso é um Serviço de Domínio?
- A lógica não pertence exclusivamente à conta de origem e nem a conta de destino pode “transferir sozinha”
- O serviço não guarda estado
- Ele expressa um verbo do domínio
- Não conhece banco, filas, APIs ou transações
O que esse serviço NÃO é
- Não é um Application Service
- Não coordena repositórios
- Não inicia transações
- Não orquestra infraestrutura
Ele executa uma regra, apenas isso.
Modelando a Dimensão do Tempo
Em muitos domínios, não basta conhecer apenas o estado atual do sistema, é necessário conseguir entender como se chegou nesse estado ao longo do tempo.
Event Sourcing
Event Sourcing é uma abordagem em que o estado de um agregado não é persistido diretamente, em vez disso, o estado é reconstruído a partir de uma sequência ordenada de eventos de domínio que representam fatos que ocorreram no passado para aquele agregado.
Cada evento descreve algo que já aconteceu no domínio e é considerado imutável. O estado atual do modelo é derivado da aplicação desses eventos, respeitando a ordem temporal. Desse jeito, o histórico completo do agregado passa a ser a fonte de verdade do sistema.
Event Sourcing não é apenas uma estratégia de persistência, mas uma forma de modelar o domínio explicitamente em torno de mudanças ao longo do tempo.
Modelo de Domínio Orientado a Eventos
O Modelo de Domínio Orientado a Eventos é o termo utilizado pelo autor Vlad Khononov para descrever modelos de domínio em que a principal abstração são eventos.
Essa abordagem está diretamente relacionada ao Event Sourcing, mas não é igual. Event Sourcing define como o estado é armazenado, o modelo orientado a eventos define como o domínio é pensado e modelado. Nesse modelo comandos expressam intenções, eventos expressam fatos e o estado é uma projeção desses fatos.
Vantagens:
- Histórico completo e auditável das decisões do negócio.
- Facilidade para depuração e entendimento de comportamentos passados.
- Melhor alinhamento com domínios onde o tempo é um fator essencial.
- Possibilidade de múltiplas projeções a partir do mesmo conjunto de eventos.
Desvantagens:
- Maior complexidade conceitual e técnica.
- Curva de aprendizado maior para a equipe.
- Maior cuidado com versionamento de eventos.
- Requisitos adicionais de infraestrutura e tooling.
Desempenho:
Reconstruir o estado a partir de todos os eventos pode impactar o desempenho, especialmente para agregados com histórico extenso. O custo de leitura costuma ser maior que o de escrita, pois consistem apenas no append de novos eventos.
Uma abordagem complementar é o uso de snapshots, que consiste em armazenar o estado atual do agregado. Durante a reconstrução, o sistema carrega o snapshot mais recente e aplica apenas os eventos posteriores, reduzindo o custo computacional.
Exemplo domínio orientado a eventos:
record OpenAccount(UUID accountId) {}
record DepositMoney(UUID accountId, BigDecimal amount) {}
record WithdrawMoney(UUID accountId, BigDecimal amount) {}
/**********/
sealed interface AccountEvent permits
AccountOpened,
MoneyDeposited,
MoneyWithdrawn {
UUID accountId();
Instant occurredAt();
}
record AccountOpened(
UUID accountId,
Instant occurredAt
) implements AccountEvent {}
record MoneyDeposited(
UUID accountId,
BigDecimal amount,
Instant occurredAt
) implements AccountEvent {}
record MoneyWithdrawn(
UUID accountId,
BigDecimal amount,
Instant occurredAt
) implements AccountEvent {}
/**********/
class Account {
private UUID id;
private BigDecimal balance = BigDecimal.ZERO;
private boolean active = false;
private final List
/_ COMANDOS → EVENTOS _/
public static Account open(OpenAccount command) {
Account account = new Account();
account.apply(
new AccountOpened(command.accountId(), Instant.now())
);
return account;
}
public void deposit(DepositMoney command) {
if (!active) {
throw new IllegalStateException(“Conta não está ativa”);
}
if (command.amount().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException(“Valor inválido”);
}
apply(new MoneyDeposited(
command.accountId(),
command.amount(),
Instant.now()
));
}
public void withdraw(WithdrawMoney command) {
if (!active) {
throw new IllegalStateException(“Conta não está ativa”);
}
if (balance.compareTo(command.amount()) < 0) {
throw new IllegalStateException(“Saldo insuficiente”);
}
apply(new MoneyWithdrawn(
command.accountId(),
command.amount(),
Instant.now()
));
}
/_ APLICAÇÃO DE EVENTOS (DERIVAÇÃO DE ESTADO) _/
private void apply(AccountEvent event) {
when(event);
changes.add(event);
}
private void when(AccountEvent event) {
if (event instanceof AccountOpened e) {
this.id = e.accountId();
this.active = true;
}
if (event instanceof MoneyDeposited e) {
this.balance = balance.add(e.amount());
}
if (event instanceof MoneyWithdrawn e) {
this.balance = balance.subtract(e.amount());
}
}
/_ RECONSTRUÇÃO DO ESTADO _/
public static Account rehydrate(List
Account account = new Account();
history.forEach(account::when);
return account;
}
public List
List
changes.clear();
return events;
}
}
- O domínio fala em eventos
- O estado é consequência
- O histórico é a fonte de verdade
- Eventos são imutáveis
- O tempo é explícito
- Projeções podem variar sem mudar o domínio
Resumo das Abordagens
Press enter or click to view image in full size

O que difere um do outro
- Modelo de Domínio vs. Script de Transação
Press enter or click to view image in full size

- Modelo de Domínio Clássico vs. Orientado a Eventos
Press enter or click to view image in full size

- Modelo de Domínio Clássico vs. Registro Ativo
Press enter or click to view image in full size

Padrões de Arquitetura
Lógica de Negócio x Padrões de Arquitetura
Padrões de arquitetura não devem dirigir o modelo de domínio, a lógica de negócio vem primeiro, a arquitetura deve servir para proteger, organizar e possibilitar sua implementação.
Arquitetura em Camadas
A arquitetura em camadas organiza o sistema separando responsabilidades em níveis diferentes.
Camadas típicas:
- Apresentação: interface com o usuário ou sistemas externos.
- Aplicação: coordena casos de uso e orquestra operações.
- Domínio: contém a lógica de negócio.
- Infraestrutura: persistência, mensageria, comunicação externa.
A comunicação ocorre de cima para baixo, com dependências direcionadas para as camadas internas.
No contexto do DDD, essa arquitetura é mais adequada quando a lógica de negócio é simples, como nos padrões Script de Transação e Registro Ativo.
Press enter or click to view image in full size

Portas e Adaptadores
O padrão Portas e Adaptadores organiza o sistema de modo que a lógica de negócio fique no centro, isolada de preocupações tecnológicas.
Esse padrão se baseia fortemente no Princípio da Inversão de Dependências, onde o domínio define interfaces (portas) e a infraestrutura fornece implementações (adaptadores).
Essa abordagem está conceitualmente alinhada com:
- Arquitetura Hexagonal (hexagonal architecture)
- Arquitetura Cebola (onion architecture)
- Arquitetura Limpa (clean architecture)
A separação completa da lógica de negócio torna Portas e Adaptadores um ótimo padrão para sistemas que utilizam o Modelo de Domínio, permitindo alta testabilidade e independência tecnológica.
Press enter or click to view image in full size

Segregação de Responsabilidade de Comando e Consulta (CQRS)
CQRS (Command Query Responsibility Segregation) é um padrão arquitetural que propõe a separação entre operações que modificam o estado do sistema (comandos) e operações que consultam o estado (queries).
Essa separação não é apenas conceitual, mas estrutural, refletindo em modelos, fluxos e até arquiteturas diferentes para leitura e escrita.
O princípio central do CQRS é entender que leitura e escrita possuem necessidades diferentes em termos de desempenho, escalabilidade, consistência e modelagem. Ao separar, o sistema pode evoluir de forma independente.
O padrão é baseado nos mesmos princípios organizacionais das Portas e Adaptadores:
- A lógica de negócio permanece isolada.
- Interfaces explícitas definem os pontos de entrada para comandos e consultas.
- Infraestrutura e tecnologia são detalhes externos ao modelo de domínio.
Conceitos Fundamentais
- Comandos representam intenções explícitas de alterar o estado do sistema. Eles expressam o que se deseja fazer, não como será feito. Um comando pode ser validado e rejeitado caso viole regras de negócio.
- Consultas representam a necessidade de obter informações do sistema. Elas não produzem efeitos colaterais e não alteram o estado do domínio.
Essa diferença elimina ambiguidades comuns em modelos tradicionais, nos quais uma mesma operação pode tanto consultar quanto modificar dados.
Características Principais
- Modelos distintos para leitura e escrita
- O modelo de escrita é orientado ao comportamento, invariantes e regras de negócio.
- O modelo de leitura é orientado à eficiência de consulta, agregações e apresentação.
- Modelagem poliglota
- Como os modelos são independentes, é possível utilizar tecnologias diferentes para cada lado: bancos relacionais ou NoSQL para escrita e bancos otimizados para leitura, busca ou analytics para consultas.
- Projeções de leitura
- O estado consultável do sistema é representado por projeções derivadas do modelo de escrita.
- Escalabilidade e desempenho
- Leitura e escrita podem escalar de forma independente, conforme suas demandas específicas.
Embora o CQRS seja frequentemente utilizado em conjunto com Event Sourcing, os dois padrões são independentes. CQRS trata da separação de responsabilidades e Event Sourcing trata da forma como o estado é persistido.
Implementação de CQRS
A implementação de CQRS envolve dois fluxos: o fluxo de comandos e o fluxo de consultas.
O modelo de comando é responsável por:
- Receber comandos expressando intenções do negócio.
- Validar regras e invariantes do domínio.
- Executar mudanças de estado de forma consistente.
- Produzir efeitos observáveis, como eventos de domínio.
Características importantes do modelo de comando:
- Um comando não retorna dados do domínio, apenas o resultado da execução (sucesso ou falha).
- A lógica de negócio está concentrada no modelo de escrita.
- O comando representa uma unidade clara de intenção e responsabilidade.
Em arquiteturas orientadas a eventos, a execução bem-sucedida de um comando frequentemente resulta na emissão de eventos.
Já no modelo de consulta, as projeções representam visões materializadas do estado do sistema, otimizadas para consulta.
Projeções Síncronas:
- Atualizadas no mesmo fluxo transacional da escrita.
- Garantem consistência imediata entre escrita e leitura.
- Introduzem maior acoplamento e impacto no tempo de resposta do comando.
São indicadas quando a consistência imediata é essencial e o volume de leitura é moderado.
Projeções Assíncronas:
- Atualizadas a partir de eventos publicados pelo modelo de escrita.
- Operam com consistência eventual.
- Reduzem acoplamento e aumentam escalabilidade.
São indicadas quando há alta taxa de leitura, baixa tolerância a impacto no desempenho da escrita e consistência eventual é aceitável.
Press enter or click to view image in full size

Escopo
Padrões arquiteturais devem ser aplicados sempre considerando o escopo correto. No DDD, esse escopo é o contexto delimitado. Um contexto inteiro pode adotar um padrão arquitetural específico, enquanto subdomínios distintos dentro do mesmo contexto podem empregar abordagens diferentes, conforme sua complexidade e importância estratégica.
Essa flexibilidade é essencial para equilibrar complexidade, custo e valor de negócio.
Padrões de Comunicação
Em DDD, a comunicação entre partes do sistema não é um detalhe técnico, mas uma extensão direta da modelagem do domínio.
Os padrões de comunicação definem como contextos delimitados trocam informações preservando a integridade de seus modelos, evitando acoplamento semântico e mantendo a consistência dos conceitos do negócio.
Tradução do Modelo
Um contexto delimitado define o limite de validade de um modelo e de sua Linguagem Ubíqua. Quando dois contextos precisam se comunicar, não é esperado que compartilhem o mesmo modelo, em vez disso se recomenda traduzir modelos explicitamente.
A tradução do modelo é o conjunto de técnicas utilizadas para converter conceitos, estruturas e significados de um contexto para outro, respeitando as diferenças semânticas entre eles.
A tradução pode ser implementada de forma sem estado ou com estado e normalmente ocorre nos limites do contexto por meio de padrões como:
- Open Host Service (OHS) para entrada
- Anti-Corruption Layer (ACL) para saída
Tradução do Modelo Sem Estado
Na Tradução do Modelo Sem Estado, cada mensagem, requisição ou evento é traduzido de forma independente, sem necessidade de manter histórico ou contexto adicional.
Essa abordagem é comumente implementada por meio de um proxy, responsável por adaptar contratos, formatos e semântica entre os contextos.
1- Design Proxy
O proxy atua como um intermediário que expõe uma interface adequada ao consumidor e traduz chamadas para o modelo interno do fornecedor. A implementação depende da natureza da comunicação.
1.1- Comunicação Síncrona
Na comunicação síncrona, o proxy normalmente é implementado como uma camada de integração HTTP ou RPC. Em alguns cenários, é mais econômico e conveniente delegar essa tradução a um componente externo, como um API Gateway.
Exemplos comuns incluem:
- Soluções open source, como Kong e KrakenD.
- Serviços gerenciados, como AWS API Gateway, Google Apigee ou Azure API Management.
Esses componentes podem realizar transformação de payloads, versionamento de contratos e validação sem expor o modelo interno do contexto.
1.2- Comunicação Assíncrona
Na comunicação assíncrona, a tradução ocorre por meio de um proxy de mensagens. Esse componente consome eventos ou mensagens de um formato e publica novos eventos traduzidos, adequados ao modelo do contexto consumidor.
Essa abordagem é especialmente útil quando diferentes contextos utilizam modelos incompatíveis, mas precisam reagir aos mesmos fatos de negócio.
Tradução do Modelo com Estado
A Tradução do Modelo com Estado é utilizada quando a tradução não pode ser feita a partir de uma única mensagem isolada.
Isso ocorre quando é necessário:
- Agregar múltiplos eventos recebidos.
- Manter correlação entre mensagens ao longo do tempo.
- Reconstruir informações de domínio a partir de uma sequência de eventos.
Nesse caso, o componente de tradução mantém estado próprio, armazenando eventos intermediários até que seja possível produzir um modelo coerente para o contexto de destino. Pode-se utilizar soluções prontas como Apache Kafka.
Integrando Agregados
Além da tradução de modelos, DDD define padrões específicos para integrar agregados de diferentes contextos de forma confiável e consistente.
Caixa de Saída (Outbox Pattern)
O padrão Caixa de Saída garante consistência entre mudanças no estado de um agregado e a publicação de eventos. Eventos de domínio são persistidos na mesma transação que a alteração do estado e posteriormente publicados de forma assíncrona, evitando inconsistências causadas por falhas parciais.
Saga
Sagas modelam processos de negócio de longa duração que envolvem múltiplos agregados e contextos delimitados.
Uma saga coordena uma sequência de transações locais, reagindo a eventos e, quando necessário, executando ações compensatórias para manter a consistência global do processo.
Gerenciador de Processo
O Gerenciador de Processo é uma especialização que centraliza a lógica de coordenação de um fluxo complexo. Diferentemente de agregados, ele não representa um conceito central do domínio, mas sim a orquestração de etapas, decisões e transições baseadas em eventos.
Esse padrão é utilizado quando o fluxo de negócio não se encaixa naturalmente dentro de um único agregado ou serviço de domínio.
Domain-Driven Design na Prática
Raramente as condições ideais para aplicar DDD estão presentes. Equipes totalmente alinhadas, domínio bem compreendido e ausência de sistemas legados são situações extremamente raras para ser honesto.
DDD não é exclusivo de projetos greenfield (projetos iniciados do zero), não é uma abordagem “tudo ou nada”. Projetos brownfield (sistemas legados em produção) frequentemente são os que mais se beneficiam do DDD, pois precisam provar viabilidade comercial, sofrem com alto débito técnico e precisam de modelos explícitos de domínio.
Análise Estratégica
O ponto de partida recomendado é compreender:
- A estratégia de negócio.
- O papel dos sistemas existentes.
- Como o software sustenta vantagens competitivas.
Entenda o Domínio de Negócio
Foque em compreender:
- Domínios
- Clientes
- Serviços
- Concorrentes
Isso fornece uma visão dos objetivos estratégicos da empresa. A identificação de subdomínios pode ser auxiliada por heurísticas como:
- Estrutura organizacional.
- Responsabilidades das equipes.
- Sistemas existentes.
Subdomínios Principais:
Para achar os subdomínios principais você pode utilizar heurísticas comuns:
- Existe um “tempero secreto” que diferencia a empresa?
- A vantagem competitiva não é puramente técnica?
- Componentes altamente complexos, difíceis de manter e impossíveis de substituir por soluções prontas?
Subdomínios Genéricos:
Já para achar os subdomínios genéricos você pode utilizar heurísticas como:
- Funções comuns ao mercado.
- Frequentemente substituíveis por soluções prontas.
Subdomínios de Suporte:
Para subdomínios de suporte:
- Não oferecem vantagem competitiva direta.
- Não podem ser facilmente terceirizados.
- Apoiam os subdomínios principais.
Explore o Design Atual
Avalie o estado atual do sistema antes de qualquer refatoração.
Avaliação do Design Tático:
- Identificar padrões de implementação da lógica de negócio.
- Detectar acoplamentos excessivos.
- Localizar inconsistências no modelo.
Avaliação do Design Estratégico:
- Avaliar limites de contextos delimitados.
- Analisar padrões de integração.
- Identificar modelos conflitantes.
Estratégia de Modernização:
Uma abordagem segura é: Pensar grande, mas começar pequeno. Comece validando ao menos limites lógicos (módulos, pacotes, namespaces).
Modernização Estratégica:
- Decompor o sistema em contextos delimitados.
- Desacoplar ciclos de desenvolvimento entre equipes.
- Separar modelos conflitantes.
- Definir padrões de integração adequados (cliente-fornecedor, ACL, OHS, caminhos separados).
Modernização Tática:
- Identificar os maiores desalinhamentos.
- Priorizar subdomínios principais implementados com padrões inadequados.
- Evoluir gradualmente o modelo.
Dentro da modernização tática você pode decidir qual a melhor estratégia para a modernização:
Padrão Estrangulador:
- Migrar funcionalidades gradualmente.
- Utilizar fachadas para encapsular sistemas legados.
- Reduzir risco durante a modernização.
Refatorando Decisões de Design Tático:
- Ajustar padrões conforme o domínio evolui.
- Evitar mudanças abruptas.
- Preservar comportamento observável.
Cultivando a Linguagem Ubíqua
Um pré-requisito para a modernização de um design é o conhecimento do domínio e o modelo eficaz do domínio de negócio, neste momento a linguagem ubíqua é essencial.
Você pode utilizar de processos como o EventStorming para construir uma linguagem ubíqua com os especialistas de domínio e explorar a base de código antiga. Reúna todas as pessoas relacionadas à sua funcionalidade e explore o domínio de negócio.
Uma vez equipado com o conhecimento do domínio e seus modelos, decida quais os padrões de implementação da lógica de negócio que melhor se adequam à funcionalidade do negócio em questão.
- Entende a estratégia de negócio.
- Escolhe modelos eficazes para resolver problemas reais.
Então você está praticando DDD!
Vendendo o DDD
Não é necessário “vender” DDD, ele não é um framework. DDD é um conjunto de técnicas de engenharia.
Use DDD como parte da sua caixa de ferramentas profissional.
Linguagem Ubíqua:
- Evite jargão técnico desnecessário.
- Questione termos ambíguos.
- Use a linguagem do negócio no código e na comunicação.
Contextos Delimitados:
- Explore opções de decomposição com base nos princípios do padrão.
- Não force limites artificiais.
Decisões de Design Tático:
- Baseie-se em lógica e evidência.
- Evite decisões por autoridade.
Modelo de Domínio Orientado a Eventos:
- Utilize quando o domínio exigir rastreabilidade, auditoria e análise temporal.
- Alinhe decisões técnicas às necessidades do negócio.
Conclusão
Este artigo apresentou um conjunto de conceitos e padrões do design tático do DDD, mostrando como traduzir decisões do design estratégico em código que expressa a lógica de negócio. No entanto, é importante reforçar que DDD não é a aplicação mecânica desses padrões.
Domain-Driven Design é um processo de engenharia focado em compreender de forma profunda o domínio. Ele coloca a comunicação no centro do desenvolvimento de software, utilizando a linguagem ubíqua como principal ferramenta para alinhar especialistas do negócio e desenvolvedores. Agregados, entidades, objetos de valor, eventos, serviços de domínio, CQRS ou Event Sourcing são apenas meios, não o fim.
Praticar DDD é:
- Investir a todo momento na linguagem ubíqua.
- Modelar o domínio com foco em comportamento e invariantes.
- Escolher padrões que sirvam ao negócio, e não o contrário.
- Evoluir o design de forma incremental e pragmática.