Domain-Driven Design
Você decidiu dividir a loja em serviços. Onde cortar? Por tabela do banco de dados (products, orders, order_items)? Por camada (um serviço para a UI, outro para a lógica)? Pelo que os times estiverem fazendo neste mês? Cada uma dessas opções produz serviços que precisam mudar juntos e se chamam o tempo todo — um monólito distribuído. Você precisa de uma forma de encontrar os pontos em que o próprio negócio tem uma divisão natural. É isso que o Domain-Driven Design oferece.
O Domain-Driven Design (DDD) é uma abordagem de desenvolvimento de software apresentada por Eric Evans1 que coloca o domínio — a área específica do negócio que o software apoia — no centro de todas as decisões de projeto. O DDD oferece um vocabulário e um conjunto de padrões que ajudam a traduzir a complexidade do negócio em estrutura de código, e é a principal ferramenta para identificar as fronteiras certas entre microsserviços.
O DDD atua em dois níveis:
| Nível | Pergunta que responde | Ferramentas |
|---|---|---|
| Design estratégico | Onde ficam as fronteiras? Como o negócio se divide em partes, e como as partes se relacionam? | Subdomínios, linguagem ubíqua, bounded contexts, mapas de contexto, Event Storming |
| Design tático | O que vai dentro de uma fronteira? Como o modelo é escrito em código? | Entidades, objetos de valor, agregados, repositórios, eventos de domínio, serviços de domínio |
O design estratégico decide os microsserviços; o design tático dá forma ao código dentro de cada um.
Subdomínios: onde investir
O domínio da loja é o varejo online. Ele se divide em subdomínios, e nem todos merecem o mesmo esforço2:
| Tipo | Significado | Na loja |
|---|---|---|
| Core (principal) | O que diferencia o negócio; vale as melhores pessoas e código sob medida | Pedidos, precificação, a experiência do catálogo |
| Supporting (de suporte) | Necessário e específico do negócio, mas não é um diferencial | Notificações, exibição de moedas |
| Generic (genérico) | Problemas já resolvidos, iguais para todas as empresas | Autenticação, pagamentos, envio de e-mails |
Subdomínios genéricos são candidatos a comprar ou reutilizar (um provedor de identidade como o Keycloak, um provedor de pagamentos). O curso constrói o próprio serviço de autenticação para aprender como ele funciona, mas uma loja real normalmente não faria isso.
Linguagem ubíqua
A prática mais importante do DDD é estabelecer uma Linguagem Ubíqua (Ubiquitous Language): um vocabulário compartilhado, usado de forma consistente por desenvolvedores e especialistas do domínio em conversas, na documentação e no código.
Sem uma linguagem ubíqua, o mesmo conceito ganha nomes diferentes em camadas diferentes:
- O negócio fala em Customer, o banco de dados tem uma tabela
USER, o código tem uma classePerson. - Cada tradução é um ponto em que o entendimento pode divergir.
Com a linguagem ubíqua, o código se lê do jeito que o negócio fala:
// Without ubiquitous language
public class Person {
private String userHandle;
public void changeUserHandle(String newHandle) { ... }
}
// With ubiquitous language (domain uses "Account" and "username")
public class Account {
private String username;
public void changeUsername(String newUsername) { ... }
}
O hands-on segue essa ideia: o negócio fala em contas (accounts), então o serviço é account-service, o recurso é /accounts e as classes são AccountIn, AccountOut e AccountService (Hands-on 1).
Bounded Context
Um Bounded Context (contexto delimitado) é a fronteira dentro da qual um determinado modelo é definido e aplicável. A mesma palavra pode significar coisas diferentes em contextos diferentes — e tudo bem, desde que cada contexto tenha uma fronteira clara.
flowchart LR
subgraph Sales ["Bounded Context: Vendas"]
c1[Customer\n- name\n- creditLimit\n- salesRegion]
end
subgraph Support ["Bounded Context: Suporte"]
c2[Customer\n- name\n- ticketHistory\n- supportTier]
end
subgraph Billing ["Bounded Context: Faturamento"]
c3[Customer\n- name\n- taxId\n- billingAddress]
end Cada bounded context tem o seu próprio modelo de Customer. Tentar construir uma única classe Customer unificada que atenda aos três leva a um objeto inchado que não atende bem a nenhum deles.
A palavra “produto” na loja
No contexto de Catálogo, um produto tem nome, preço atual e unidade, e o preço pode mudar a qualquer dia. No contexto de Pedidos, o que importa é o produto como foi vendido: o seu id, a quantidade e o total pago. É por isso que, na Order API, cada item de pedido guarda o seu próprio total e apenas o id do produto: se o preço no catálogo mudar amanhã, o pedido de ontem não pode mudar. A mesma palavra, dois modelos, dois serviços.
Em microsserviços, um bounded context normalmente corresponde a um microsserviço (ou a um pequeno grupo de serviços muito relacionados). É por isso que o DDD é a principal ferramenta para decompor microsserviços — o bounded context dá a divisão natural.
Mapas de contexto: como os contextos se relacionam
Bounded contexts não são ilhas; eles trocam dados. Um mapa de contexto (context map) desenha os contextos e dá nome à relação entre cada par3. Em toda relação, um lado é upstream (o seu modelo influencia o outro) e o outro é downstream (depende do modelo upstream).
| Padrão | Significado | Use quando |
|---|---|---|
| Customer / Supplier | As necessidades do time downstream são levadas em conta nos planos do time upstream | Dois times da mesma empresa que conversam entre si |
| Open Host Service + Published Language | O upstream expõe uma API bem definida e documentada (por exemplo, REST + JSON descrito em OpenAPI) para qualquer consumidor | Um provedor, muitos consumidores |
| Conformist | O downstream simplesmente adota o modelo upstream como está | O modelo upstream é bom o suficiente, ou você não tem influência sobre ele |
| Anti-Corruption Layer (ACL) | O downstream escreve uma camada de tradução para que o modelo upstream não vaze para o seu | O upstream é um sistema legado ou de terceiros, com um modelo que você não quer |
| Shared Kernel | Dois contextos compartilham uma pequena parte do modelo (e o seu código), alterada apenas em comum acordo | Dois times que cooperam de perto; use com moderação — isso os acopla |
O mapa de contexto da loja:
flowchart LR
product["Catálogo (product)"] -->|"Open Host Service: REST + JSON"| order["Pedidos (order)"]
account["Contas (account)"] -->|"Conformist: AccountOut"| auth["Identidade (auth)"]
rates["API de cotações de terceiros"] -->|"Anti-Corruption Layer"| exchange["Câmbio (exchange)"]
exchange -->|"Open Host Service: REST"| order
order -->|"evento OrderCreated"| notification["Notificação"] - Catálogo → Pedidos: o serviço de produtos publica a sua API (um Open Host Service, documentado com OpenAPI); o serviço de pedidos a consome.
- Contas → Identidade: o serviço de autenticação reutiliza
AccountIn/AccountOutdo móduloaccountexatamente como estão — uma relação conformista. - Cotações de terceiros → Câmbio: o serviço de câmbio não pode deixar o formato JSON do provedor se espalhar pelo seu código. Ele o traduz na fronteira:
def get_rate(source: str, target: str) -> Rate:
# the provider's model, e.g. {"base": "USD", "rates": {"BRL": 5.43}, "date": "..."}
raw = provider.latest(base=source)
# our model: one Rate value object, in our own vocabulary
return Rate(source=source, target=target, value=Decimal(str(raw["rates"][target])))
Se o provedor mudar o formato, ou for substituído, só esta função muda.
Blocos de construção
Os padrões táticos, ilustrados com o contexto de Pedidos.
Entidades
Objetos com uma identidade própria, que persiste ao longo do tempo e entre diferentes representações. Duas entidades são a mesma se têm a mesma identidade, independentemente de os seus atributos serem diferentes.
public class Order {
private final OrderId id; // identity
private OrderStatus status;
private List<OrderItem> items;
// Two Orders with the same id are the same Order,
// even if their status differs.
}
Objetos de valor
Objetos definidos inteiramente pelos seus atributos — eles não têm identidade. Dois objetos de valor (value objects) com os mesmos atributos são intercambiáveis.
public record Money(BigDecimal amount, Currency currency) {
// No id field. Two Money(10.00, BRL) objects are equal and interchangeable.
public Money add(Money other) {
if (!this.currency.equals(other.currency))
throw new CurrencyMismatchException();
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
}
}
Objetos de valor devem ser imutáveis. As operações retornam novas instâncias em vez de alterar o estado. Os records do Java se encaixam naturalmente: os campos são finais e equals/hashCode comparam os atributos.
Agregados
Um Agregado (Aggregate) é um grupo de entidades e objetos de valor tratado como uma única unidade para alterações de dados. Todo agregado tem uma Raiz do Agregado (Aggregate Root) — o único ponto de entrada para modificações.
flowchart TB
subgraph Order [Agregado: Order]
root[Order\n«raiz»]
item1[OrderItem]
item2[OrderItem]
addr[ShippingAddress\n«objeto de valor»]
root --> item1
root --> item2
root --> addr
end Regras:
- O código externo guarda referências apenas para a raiz, nunca para as entidades internas.
- Todas as invariantes são garantidas pela raiz. Exemplo: "um pedido não pode ter mais de 50 itens" é verificado dentro de
Order.addItem(). - Agregados são a unidade de persistência — salve e carregue o agregado inteiro de uma vez.
- Referencie outros agregados apenas pela identidade. Um
OrderItemguarda o id do produto, não um objetoProduct; o pedido guarda o id da conta, não umAccount. Caso contrário, um agregado poderia alterar outro, e os dois nunca poderiam viver em serviços diferentes. - Uma transação altera um agregado. Se um caso de uso precisa alterar dois agregados, a segunda alteração acontece depois, disparada por um evento de domínio (consistência eventual).
Uma invariante é uma regra que precisa ser sempre verdadeira para que o agregado seja válido. A raiz é onde ela é verificada:
public class Order { // aggregate root
private static final int MAX_ITEMS = 50;
private final OrderId id;
private final AccountId accountId; // other aggregate: by id only
private final List<OrderItem> items = new ArrayList<>();
private Money total = new Money(BigDecimal.ZERO, Currency.getInstance("USD"));
public void addItem(ProductId productId, int quantity, Money unitPrice) {
if (quantity <= 0)
throw new IllegalArgumentException("quantity must be positive");
if (items.size() >= MAX_ITEMS)
throw new IllegalStateException("an order has at most " + MAX_ITEMS + " items");
OrderItem item = new OrderItem(productId, quantity, unitPrice.multiply(quantity));
items.add(item);
total = total.add(item.total()); // invariant: total = sum of items
}
public List<OrderItem> items() {
return List.copyOf(items); // callers cannot bypass the root
}
}
Repositórios
Um Repositório (Repository) oferece uma interface semelhante a uma coleção para buscar e persistir agregados. Os repositórios escondem todos os detalhes do banco de dados do modelo de domínio.
public interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomer(CustomerId customerId);
Order save(Order order);
}
O domínio declara a interface; a infraestrutura fornece a implementação (JPA, MongoDB, em memória). No hands-on, AccountRepository extends CrudRepository<AccountModel, String> é uma interface desse tipo, e o Spring Data gera a implementação. Existe um repositório por raiz de agregado — não existe OrderItemRepository, porque os itens só são salvos por meio do seu pedido.
Eventos de domínio
Um Evento de Domínio (Domain Event) é o registro de algo que aconteceu no domínio — no passado, imutável. Eventos são o principal mecanismo para desacoplar bounded contexts.
Quando o agregado Order é efetivado, ele emite OrderPlaced. Outros bounded contexts (Estoque, Faturamento, Notificações) assinam esse evento e reagem de forma independente — sem que o contexto de Pedidos saiba quem está ouvindo. No Hands-on 9, esse é o evento OrderCreated que o serviço de pedidos publica no tópico Kafka order-events e que o serviço de notificação consome.
Serviços de domínio
Operações que não pertencem naturalmente a nenhuma entidade ou objeto de valor — em geral, envolvem vários agregados ou exigem informações externas.
// Neither Order nor Customer "owns" this logic — it needs both
public class PricingService {
public Money calculateTotal(Order order, CustomerDiscount discount) { ... }
}
Usando DDD para encontrar as fronteiras dos microsserviços
O Event Storming (Alberto Brandolini)4 é a técnica mais eficaz para descobrir bounded contexts de forma colaborativa. Especialistas do domínio e desenvolvedores se reúnem diante de uma parede de post-its e mapeiam todo o domínio em uma linha do tempo compartilhada.
flowchart LR
ev1["🟠 OrderPlaced\n(evento de domínio)"]
ev2["🟠 PaymentCharged"]
ev3["🟠 InventoryReserved"]
ev4["🟠 OrderShipped"]
cmd1["🔵 Fazer pedido\n(comando)"]
cmd2["🔵 Cobrar cartão"]
cmd3["🔵 Reservar itens"]
cmd1 --> ev1 --> cmd2 & cmd3
cmd2 --> ev2
cmd3 --> ev3
ev2 & ev3 --> ev4 O processo:
- Identifique os eventos de domínio (laranja) — coisas que aconteceram, no passado: OrderPlaced, PaymentFailed, ItemShipped
- Adicione os comandos (azul) — o que disparou cada evento: Fazer pedido, Cobrar cartão
- Agrupe por bounded context — junte eventos e comandos que pertencem à mesma linguagem ubíqua
- Desenhe os mapas de contexto — identifique como os contextos interagem: shared kernel, relação customer/supplier ou anti-corruption layer (veja Mapas de contexto)
- Um contexto → um serviço como ponto de partida; junte ou divida com base em restrições operacionais e de time
Bounded context ≠ microsserviço (sempre)
Um bounded context é uma fronteira conceitual. Na prática, um time pequeno pode implementar dois bounded contexts relacionados em um único serviço para evitar o custo operacional de um deploy separado. Só divida quando a independência de deploy ou de escala passar a ter valor.
Erros comuns de DDD
- Serviços de entidade: um serviço por tabela ou substantivo (
OrderItemService,AddressService). Eles não têm comportamento próprio e precisam sempre ser chamados juntos. - Modelo de domínio anêmico: entidades só com getters e setters, e todas as regras em uma classe de serviço. Nada protege as invariantes; coloque a regra na raiz do agregado.
- Um modelo gigante: uma única classe
Customer/Productcompartilhada por todos os serviços por meio de uma biblioteca comum — toda mudança obriga todos os serviços a um novo deploy. - Agregados grandes demais: um agregado
Accountque contém todos os seus pedidos. Carregar uma conta carrega milhares de pedidos, e dois usuários editando pedidos diferentes entram em conflito. Mantenha os agregados pequenos; ligue-os pelo id.
Pontos-chave
- O DDD estratégico encontra as fronteiras (subdomínios, bounded contexts, mapas de contexto); o DDD tático dá forma ao código dentro de cada uma.
- Uma linguagem ubíqua faz com que negócio, código e API usem as mesmas palavras (
Account,/accounts). - Um bounded context é onde um modelo é válido — o candidato natural a um microsserviço.
- Os mapas de contexto dão nome às relações; uma anti-corruption layer mantém o modelo de um terceiro fora do seu código.
- Os agregados protegem as invariantes por meio da raiz, referenciam outros agregados pelo id e são alterados em uma transação cada.
- Os eventos de domínio (no passado) permitem que outros contextos reajam sem que o produtor os conheça.
Conexões
- Por que as fronteiras importam: Microsserviços. A infraestrutura ao redor delas: Arquitetura e componentes.
- Mantendo o domínio independente de frameworks dentro de um serviço: Clean Architecture e Arquitetura Hexagonal.
- Eventos de domínio em um broker: Arquitetura orientada a eventos, Hands-on 9 — Kafka.
- Modele os seus próprios contextos: Product API, Order API, Exchange API.
-
EVANS, E. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003. ↩
-
VERNON, V. Implementing Domain-Driven Design. Addison-Wesley, 2013. ↩
-
Domain-Driven Design Reference — cartão de referência condensado de Eric Evans. ↩
-
BRANDOLINI, A. Introducing Event Storming. eventstorming.com. ↩