Introducao
"Eu mais ou menos sei o que e DDD, mas o que de fato preciso fazer para algo ser considerado DDD?"
"Qual e a diferenca real entre uma Entity e um Value Object?"
"Se eu criar um Repository, isso ja faz ser DDD?"
Muita gente provavelmente tem perguntas assim. Neste artigo, partindo de Domain-Driven Design de Eric Evans e Implementing Domain-Driven Design de Vaughn Vernon, vou reorganizar a essencia de DDD em uma forma que de fato possa ser usada na pratica.
As explicacoes dos termos principais estao alinhadas o maximo possivel com o DDD Reference de Eric Evans e com a visao geral oficial de Vaughn Vernon.
O que e DDD?
Domain-Driven Design (DDD) e uma abordagem de design baseada na ideia de que, ao desenvolver software com logica de negocio complexa,
o dominio deve ficar no centro do software, e o codigo deve ser escrito na linguagem desse dominio
O ponto-chave e que DDD nao e nem um framework nem uma arquitetura. E uma colecao de ideias e tecnicas. Por isso, adicionar uma biblioteca de DDD nao faz seu sistema se tornar automaticamente "DDD".
DDD se divide, em linhas gerais, em duas camadas
DDD pode ser dividido nas duas partes a seguir.
| Categoria | Tambem chamada | O que faz |
|---|---|---|
| Design estrategico | Strategic Design | Divide o dominio de forma apropriada e desenha limites claros |
| Design tatico | Tactical Design | Expressa o dominio no nivel do codigo |
E facil pensar que "DDD significa criar Entities e Value Objects", mas isso cobre apenas o lado tatico. O que realmente importa ainda mais e o design estrategico.
Design estrategico
Ubiquitous Language
Este e o ponto de partida de DDD e seu conceito mais importante.
Especialistas de dominio, desenvolvedores e stakeholders usam as mesmas palavras
Por exemplo, quando pessoas de um e-commerce dizem "order":
- Marketing: "O momento em que um item entra no carrinho ja e um pedido"
- Financeiro: "O momento em que o pagamento e concluido e um pedido"
- Logistica: "O momento em que a instrucao de envio e emitida e um pedido"
E muito comum que o significado varie de pessoa para pessoa. Se voce leva essa ambiguidade direto para o codigo, ninguem mais sabe o que a classe Order deveria significar.
Faca do modelo a espinha dorsal da linguagem e continue usando essa linguagem tanto na conversa quanto no codigo. Essa e a base de DDD.
Bounded Context
Uma ubiquitous language nao precisa ser unificada em toda a organizacao e, na verdade, geralmente nao consegue ser.
Por isso, voce desenha um limite que diz: "Dentro deste escopo, esta palavra significa isto". Isso e um Bounded Context. O limite nao e apenas um acordo de conversa. Ele tambem aparece na estrutura da equipe, no alcance em que o modelo se aplica dentro da aplicacao e na separacao de codebases e data stores.
flowchart LR
subgraph Sales["Contexto de vendas"]
S["Order =<br/>Intencao de compra apos o checkout"]
end
subgraph Logistics["Contexto logΓstico"]
L["Order =<br/>Pacote pronto para expedicao"]
end
N["A mesma Order pode significar coisas diferentes"]
S -.-> N
L -.-> N
classDef context fill:#eef2ff,stroke:#6366f1,color:#312e81
classDef note fill:#ffffff,stroke:#94a3b8,color:#334155,stroke-dasharray: 5 5
class S,L context
class N noteO mapa desses limites e o context map. Ele tambem e uma orientacao muito forte quando chega a hora de decidir limites de microservicos.
Como extrair um dominio a partir dos requisitos
Em algum momento todo mundo pergunta: "Entao como eu encontro de fato o dominio?" Aqui estao algumas abordagens que realmente ajudam em projetos reais.
1. Trate entrevistas como uma oportunidade de coletar verbos e substantivos
Ao conversar com clientes ou pessoas do negocio, separe conscientemente os substantivos = modelos candidatos dos verbos = comportamentos candidatos e registre assim.
"Um representante comercial cria uma cotacao, a envia para o cliente e, quando ela e aprovada, transforma isso em um pedido."
So dessa frase, ja da para identificar:
- Substantivos: representante comercial, cotacao, cliente, pedido -> candidatos a Entities / Value Objects
- Verbos: criar, enviar, aprovar, transformar em pedido -> candidatos a comportamentos / domain events
As palavras que as pessoas do negocio usam sem perceber sao o minerio bruto da ubiquitous language.
2. Use EventStorming
Para dominios complexos, EventStorming, em que voce organiza eventos em post-its numa lousa ou em ferramentas como Miro e FigJam, e extremamente eficaz.
flowchart LR
E1["π§ Cotacao solicitada"] --> E2["π§ Cotacao criada"] --> E3["π§ Cotacao enviada"]
E3 --> C2["π¦ Comando<br/>Aprovar cotacao"] --> E4["π§ Cotacao aprovada"] --> E5["π§ Pedido aceito"]
E3 --> C1["π¦ Comando<br/>Cancelar cotacao"] --> E6["π§ Cotacao cancelada"]
A1["πͺ Aggregate<br/>Cotacao"] -.dispara os eventos.-> E2
R1["π¨ Regra<br/>Apenas cotacoes aprovadas podem ser convertidas em pedidos"] -.restricao.-> E5
classDef event fill:#fdba74,stroke:#ea580c,color:#7c2d12
classDef command fill:#bfdbfe,stroke:#2563eb,color:#1e3a8a
classDef aggregate fill:#ddd6fe,stroke:#7c3aed,color:#4c1d95
classDef rule fill:#fde68a,stroke:#ca8a04,color:#713f12
class E1,E2,E3,E4,E5,E6 event
class C1,C2 command
class A1 aggregate
class R1 rule- π§ Post-its laranja: domain events, escritos no passado
- π¦ Post-its azuis: commands, ou seja, a acao de alguem
- πͺ Post-its roxos: aggregates, as coisas que disparam os eventos. Exemplo:
Cotacao - π¨ Post-its amarelos: regras de negocio / restricoes. Exemplo: "Apenas cotacoes aprovadas podem ser convertidas em pedidos"
Por exemplo, "Aprovar cotacao" e um comando porque e uma instrucao que alguem executa. Em contraste, "Cotacao aprovada" e um domain event porque e um fato que ja aconteceu.
Fazer isso junto com pessoas do negocio faz com que o conhecimento tacito fique visivel de repente. O maior valor esta no fato de as conversas surgirem naturalmente: "Ah, entao uma decisao acontece aqui?" "Na verdade, neste caso existe uma excecao..."
3. Sempre pergunte sobre casos excepcionais
A fonte mais rica de informacao em entrevistas de requisitos e o fluxo de excecao.
| Pergunta | O que revela |
|---|---|
| "Em que situacoes isso nao pode ser feito?" | Invariantes e regras de negocio |
| "Que tipos de casos geraram conflitos no passado?" | Restricoes ocultas |
| "O que as pessoas estao contornando manualmente?" | Regras de negocio que nunca foram implementadas |
| "O que mudou nesta operacao nos ultimos cinco anos?" | Partes volateis versus partes estaveis |
Se voce so pergunta pelo happy path, o formato do dominio nunca aparece por completo. A essencia do dominio se esconde nas partes dolorosas.
4. Classifique em core, supporting e generic
Nem todo dominio extraido precisa ser implementado com o mesmo peso.
| Categoria | Descricao | Estrategia |
|---|---|---|
| Core Domain | A origem da vantagem competitiva do negocio | Construir internamente com cuidado usando DDD |
| Supporting Subdomain | Capacidades de negocio que sustentam o nucleo | Implementar internamente de forma leve ou terceirizar |
| Generic Subdomain | Comum a qualquer empresa, como autenticacao ou pagamentos | Usar SaaS / OSS |
Tomando um e-commerce como exemplo:
- Core: recomendacao de produtos, estrategia de precos, alocacao de estoque
- Supporting: gestao de catalogo de produtos, coordenacao de entrega
- Generic: autenticacao, pagamentos, envio de email
Se voce tentar caprichar igualmente em tudo, o projeto vai desmoronar. Deixar explicito onde vale investir esforco e o primeiro passo do design de limites.
5. Criterios para desenhar limites de contexto
Decidir onde desenhar limites exige experiencia, mas os sinais a seguir ajudam bastante.
- π© A mesma palavra muda de significado (o exemplo de
Orderno inicio) - π© A organizacao ou departamento responsavel muda (Lei de Conway)
- π© O ciclo de vida e diferente (pedidos e clientes nao vivem na mesma linha do tempo)
- π© A velocidade de mudanca e diferente (estrategia de precos muda bastante; gestao de endereco, nem tanto)
- π© O nivel de consistencia exigido e diferente (estoque precisa de consistencia forte; recomendacoes toleram consistencia eventual)
Quando voce percebe um desses desalinhamentos, provavelmente encontrou um limite candidato.
Design tatico
Daqui em diante, vamos para o codigo.
Value Object
Um Value Object representa "o proprio valor" e tem as seguintes caracteristicas:
- Imutavel
- Comparado por igualdade de valor, e nao por identidade
- Nao tem efeitos colaterais
// β Apenas uma string
const email: string = "user@example.com";
// β
Value Object
class Email {
constructor(private readonly value: string) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error("Endereco de email invalido");
}
}
equals(other: Email): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}
A ideia e encapsular a restricao no tipo que diz "isto e um endereco de email valido". A validacao deixa de ficar espalhada pelo codigo.
Entity
Uma Entity e um objeto cuja identidade e determinada por um identificador (ID).
class User {
constructor(
public readonly id: UserId,
private name: UserName,
private email: Email,
) {}
changeEmail(newEmail: Email): void {
this.email = newEmail;
}
equals(other: User): boolean {
return this.id.equals(other.id);
}
}
Mesmo que o nome ou o email mudem, se o id for o mesmo, continua sendo o mesmo User. Essa e a essencia de uma Entity.
Aggregate
Um Aggregate e um limite de consistencia que agrupa varias Entities e Value Objects em uma unica unidade.
A entidade que funciona como ponto de entrada e chamada de Aggregate Root, e o codigo externo deve sempre acessar o aggregate por essa raiz.
// Aggregate root
class Order {
private items: OrderItem[] = [];
constructor(
public readonly id: OrderId,
private readonly userId: UserId,
) {}
// O aggregate root protege a consistencia dentro do aggregate.
addItem(product: Product, quantity: number): void {
if (this.items.length >= 100) {
throw new Error("O limite de itens por pedido foi excedido");
}
this.items.push(new OrderItem(product.id, quantity, product.price));
}
totalPrice(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.subtotal()),
Money.zero(),
);
}
}
Uma regra fundamental e: mantenha os aggregates o menores possivel. Se eles crescerem demais, voce vai sofrer com conflitos de atualizacao e custo de carregamento.
Repository
Um Repository e uma abstracao responsavel por persistir aggregates. Ele existe para que a camada de dominio nao dependa da infraestrutura.
// Camada de dominio: define apenas a interface.
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
}
// Camada de infraestrutura: coloca a implementacao fora.
class PostgresOrderRepository implements OrderRepository {
async findById(id: OrderId): Promise<Order | null> { /* ... */ }
async save(order: Order): Promise<void> { /* ... */ }
}
O importante aqui e que um Repository e uma abstracao que oferece acesso global a aggregate roots e permite que quem a chama os trate como uma colecao. Nao pense nisso como "algo que grava no banco de dados", mas como "a porta de entrada para armazenar e recuperar aggregates".
Domain Service
Um Domain Service e o lugar onde voce coloca processos ou transformacoes importantes do dominio que nao se encaixam de forma natural como responsabilidades de uma Entity ou de um Value Object.
// Uma verificacao de email duplicado nao pode ser decidida por User sozinho.
class UserDuplicationChecker {
constructor(private readonly userRepository: UserRepository) {}
async exists(email: Email): Promise<boolean> {
return (await this.userRepository.findByEmail(email)) !== null;
}
}
Um fluxo de decisao para quando surgir a duvida: Entity ou Service?
Durante a implementacao, uma das perguntas mais comuns e: "Essa logica deve ficar na Entity ou em um Service?" O fluxo a seguir resolve a maioria dos casos.
flowchart TB
Start["A logica..."] --> Q1{"so muda o estado de um unico objeto?"}
Q1 -- SIM --> E["Metodo da Entity"]
Q1 -- NAO --> Q2{"toma uma decisao atraves de varios aggregates?"}
Q2 -- SIM --> D["Domain Service"]
Q2 -- NAO --> Q3{"exige um repository ou servico externo?"}
Q3 -- SIM --> D
Q3 -- NAO --> Q4{"soa mais natural como verbo do que como nome?"}
Q4 -- SIM --> D
Q4 -- NAO --> V["Entity / Value Object"]
classDef question fill:#eef2ff,stroke:#6366f1,color:#312e81
classDef answer fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
class Q1,Q2,Q3,Q4 question
class E,D,V answerβ O que deve ficar em uma Entity
A logica que protege seu proprio estado e seus invariantes pertence a Entity.
class Order {
private status: OrderStatus;
private items: OrderItem[];
// β
Protege suas proprias transicoes de estado.
cancel(): void {
if (this.status === OrderStatus.SHIPPED) {
throw new Error("Um pedido enviado nao pode ser cancelado");
}
this.status = OrderStatus.CANCELLED;
}
// β
Logica que so calcula a partir dos proprios dados.
totalPrice(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.subtotal()),
Money.zero(),
);
}
// β
Verifica seu proprio invariante.
addItem(item: OrderItem): void {
if (this.items.length >= 100) throw new Error("O limite foi excedido");
this.items.push(item);
}
}
Um criterio util e este: se soar natural personificar e perguntar "Order, voce pode ser cancelada?", entao provavelmente isso e responsabilidade da Entity.
β O que deve ficar em um Domain Service
A logica que nao pode ser decidida apenas por uma unica Entity, ou que exige interacao com o mundo externo, pertence a um Domain Service.
// β
Logica de transferencia que cruza dois aggregates.
class TransferService {
transfer(from: Account, to: Account, amount: Money): void {
from.withdraw(amount);
to.deposit(amount);
// O conceito de transferencia nao pertence a apenas uma conta.
}
}
// β
Uma verificacao de duplicidade que exige um repository.
class UserDuplicationChecker {
constructor(private readonly userRepository: UserRepository) {}
async exists(email: Email): Promise<boolean> {
return (await this.userRepository.findByEmail(email)) !== null;
}
}
Outro criterio util e: se voce perguntar "User, voce sabe se este endereco de email ja esta cadastrado por outra pessoa?" e a resposta natural for "Como eu saberia?", entao isso pertence a um Service.
β Antipadrao: colocar tudo em um Service
// β Nao faca isso.
class OrderService {
cancel(order: Order): void {
if (order.status === "SHIPPED") throw new Error(/* ... */);
order.status = "CANCELLED"; // reescrita via setter
}
totalPrice(order: Order): Money { /* percorrer itens e somar */ }
addItem(order: Order, item: OrderItem): void { /* ... */ }
}
class Order {
status: string;
items: OrderItem[];
// apenas getters / setters β um anemic domain model
}
Isso e um transaction script tipico. A Entity virou pouco mais que uma estrutura de dados. O principio e: se um comportamento pode ser escrito tendo Order como sujeito, escreva-o dentro de Order.
Em resumo
| Perspectiva | Coloque em Entity | Coloque em Domain Service |
|---|---|---|
| Sujeito | O proprio objeto | Um verbo ou acao de negocio |
| Estado | Muda seu proprio estado | Stateless |
| Dependencias | Nao depende de Repository | Pode depender de Repositories / sistemas externos |
| Escopo | Dentro de um unico aggregate | Entre varios aggregates |
| Exemplo | order.cancel() |
transferService.transfer(a, b, money) |
Domain Event
Um Domain Event e um objeto que representa "algo importante que aconteceu no dominio". Ele se encaixa muito bem com microservicos e processamento assincrono, por isso ganhou bastante atencao nos ultimos anos.
class OrderPlaced {
constructor(
public readonly orderId: OrderId,
public readonly userId: UserId,
public readonly occurredAt: Date,
) {}
}
Ele permite separar efeitos colaterais de forma desacoplada, por exemplo: "pedido realizado -> enviar email" ou "pedido realizado -> conceder pontos".
Arquitetura em camadas
DDD costuma ser implementado com uma estrutura de camadas como a seguinte.
flowchart TB
P["Camada de Apresentacao<br/>UI / API"]
A["Camada de Aplicacao<br/>Casos de uso"]
D["Camada de Dominio<br/>Modelo / Regras<br/>Ator principal"]
I["Camada de Infraestrutura<br/>DB / API"]
P --> A --> D
I --> D
classDef layer fill:#eef2ff,stroke:#6366f1,color:#312e81
classDef domain fill:#c7d2fe,stroke:#4338ca,color:#1e1b4b
class P,A,I layer
class D domainAs dependencias sempre apontam para dentro. A camada de Dominio nao depende de nenhuma das outras. Para impor isso com rigor, as pessoas costumam usar Onion Architecture ou Hexagonal Architecture (Ports and Adapters).
Deixe explicita a responsabilidade de cada camada
No desenvolvimento real, uma das formas mais faceis de criar bagunca e o problema de a logica vazar por todas as camadas. Ajuda bastante lembrar o papel de cada camada em uma frase.
| Camada | Responsabilidade | O que nao deve fazer |
|---|---|---|
| Apresentacao | Receber requisicoes e apresentar resultados | Escrever regras de negocio |
| Aplicacao | Coordenar casos de uso | Escrever regras de negocio |
| Dominio | Conter as regras de negocio em si | Depender de frameworks ou banco de dados |
| Infraestrutura | Implementar persistencia e comunicacao externa | Tomar decisoes de negocio |
A restricao-chave e forte e simples: regras de negocio pertencem apenas a camada de Dominio. Se voce mantiver essa regra, a logica de negocio sobrevive mesmo quando o framework muda.
O papel da camada de Apresentacao
Ela nao deveria fazer nada alem de receber entrada de usuarios ou outros sistemas, passΓ‘-la para a camada de Aplicacao e devolver o resultado. A regra e: mantenha-a o mais fina possivel.
// β
Bom controller: ele apenas faz a passagem.
@Controller("orders")
class OrderController {
constructor(private readonly placeOrder: PlaceOrderUseCase) {}
@Post()
async create(@Body() body: PlaceOrderRequest): Promise<OrderResponse> {
// 1. Dar forma a requisicao (converter em DTO).
const command = new PlaceOrderCommand(
body.userId,
body.items.map(i => ({ productId: i.productId, quantity: i.quantity })),
);
// 2. Delegar para o caso de uso.
const result = await this.placeOrder.execute(command);
// 3. Dar forma a resposta e devolver.
return OrderResponse.from(result);
}
}
O que a camada de Apresentacao deve fazer:
- Fazer parsing e validacao do formato da requisicao, como campos obrigatorios, tipos e tamanho de strings
- Executar autenticacao e autorizacao no ponto de entrada
- Converter resultados para o formato da resposta
- Escolher os codigos de status HTTP
O que ela nao deve fazer e julgamento de negocio, como "devolver 400 se nao houver estoque". Isso e trabalho da camada de Dominio. Apresentacao apenas traduz para HTTP o resultado produzido pelo Dominio.
O papel da camada de Aplicacao
Esta e a camada mais mal compreendida em DDD. Um Application Service e um coordenador de casos de uso. Nao contem logica de negocio.
// β
Bom Application Service: ele so coordena o fluxo.
class PlaceOrderUseCase {
constructor(
private readonly userRepository: UserRepository,
private readonly productRepository: ProductRepository,
private readonly orderRepository: OrderRepository,
private readonly inventoryService: InventoryService, // Domain Service
private readonly transactionManager: TransactionManager,
private readonly eventPublisher: EventPublisher,
) {}
async execute(command: PlaceOrderCommand): Promise<OrderResult> {
return this.transactionManager.run(async () => {
// 1. Carregar os aggregates necessarios.
const user = await this.userRepository.findById(command.userId);
if (!user) throw new UserNotFoundError(command.userId);
const products = await this.productRepository.findByIds(
command.items.map(i => i.productId),
);
// 2. Delegar o processamento para o dominio (as regras vivem em Domain).
const order = Order.place(user, products, command.items);
// 3. Chamar o domain service (decisao que atravessa varios aggregates).
await this.inventoryService.reserve(order);
// 4. Persistir.
await this.orderRepository.save(order);
// 5. Publicar domain events.
await this.eventPublisher.publishAll(order.pullEvents());
return OrderResult.from(order);
});
}
}
Estas sao as cinco responsabilidades de um Application Service.
- Carregar aggregates chamando Repositories
- Delegar para o dominio deixando que Entities ou Domain Services executem as regras
- Gerenciar limites de transacao
- Mandar persistir salvando por meio de Repositories
- Publicar domain events
Em contraste, este e um exemplo tipico de codigo que nao deve viver em um Application Service:
// β Antipadrao: regras de negocio vazando para a camada de Aplicacao.
class PlaceOrderUseCase {
async execute(command: PlaceOrderCommand) {
// β Verificar estoque aqui vaza conhecimento de dominio.
if (product.stock < command.quantity) {
throw new Error("Sem estoque");
}
// β Calcular o total aqui deveria ser trabalho de Order.
const total = command.items.reduce((s, i) => s + i.price * i.quantity, 0);
// β Decidir sobre estado aqui deveria ser trabalho de Order.
if (user.status === "BLOCKED") throw new Error("Conta suspensa");
await this.orderRepository.save({ /* ... */ });
}
}
Um bom teste e: "Se eu mostrar este codigo para alguem do negocio, essa pessoa consegue enxergar as regras de negocio nele?" Se as decisoes de negocio estao sendo tomadas em if nesta camada, e sinal de que essa logica deve ir para o Dominio.
O fluxo de uma requisicao atraves das camadas
Vamos acompanhar o fluxo de quando um usuario clica no botao de fazer pedido durante o desenvolvimento real.
sequenceDiagram
participant Browser
participant Presentation as Apresentacao / OrderController
participant Application as Aplicacao / PlaceOrderUseCase
participant Domain as Dominio / Order.place(...)
participant Infrastructure as Infraestrutura / Implementacao do repository
Browser->>Presentation: POST /orders
Presentation->>Application: Passar PlaceOrderCommand
Note over Application: Iniciar transacao
Application->>Infrastructure: userRepository.findById()
Application->>Infrastructure: productRepository.findByIds()
Infrastructure-->>Application: User / Product aggregates
Application->>Domain: Order.place(user, products, items)
activate Domain
Domain->>Domain: Validar regras de negocio
Domain->>Domain: Verificar invariantes
Domain->>Domain: Criar evento OrderPlaced
Domain-->>Application: Order aggregate e OrderPlaced
deactivate Domain
Application->>Domain: inventoryService.reserve(order)
Domain-->>Application: Resultado da reserva de estoque
Application->>Infrastructure: orderRepository.save(order)
Application->>Infrastructure: eventPublisher.publishAll(...)
Infrastructure-->>Application: Persistencia concluida
Note over Application: Confirmar transacao
Application-->>Presentation: OrderResult
Presentation-->>Browser: 201 CreatedComo este fluxo mostra, as decisoes de negocio acontecem apenas dentro da camada de Dominio. As outras camadas so fazem a preparacao e a limpeza necessarias para que o Dominio faca bem o seu trabalho.
O limite entre DTOs e objetos de dominio
Ao atravessar camadas, a pratica padrao e nao expor objetos de dominio diretamente para fora.
// Camada de dominio
class Order { /* Aggregate com logica de negocio */ }
// Camada de aplicacao: DTOs de entrada / saida
class PlaceOrderCommand { /* DTO de requisicao */ }
class OrderResult { /* DTO de resposta */ }
// Camada de apresentacao: tipos voltados para HTTP
class PlaceOrderRequest { /* esquema JSON */ }
class OrderResponse { /* JSON de resposta */ }
Pode parecer trabalhoso, mas isso traz bastante coisa boa:
- O Dominio continua intacto mesmo que o formato do API mude
- As preocupacoes de serializacao deixam de poluir o dominio
- Mudar para GraphQL ou gRPC vira uma alteracao localizada
Em especial, nunca adicione decoradores de ORM como @Column ou anotacoes de serializacao como @Expose diretamente em um Domain Object. No momento em que voce faz isso, o Dominio passa a depender do framework.
Ordem recomendada de implementacao
Por fim, aqui esta uma ordem de trabalho recomendada ao implementar uma funcionalidade nova com DDD.
- Escreva o caso de uso em uma frase: "Um usuario compra um produto"
- Extraia os termos que aparecem: usuario / produto / pedido / estoque
- Decida os aggregates e seus limites: o que pertence ao aggregate
Order - Comece pela camada de Dominio: Entity, Value Object, Domain Service
- Escreva testes da camada de Dominio (eles devem rodar sem banco de dados)
- Escreva o Application Service: continue usando Repositories como interfaces
- Implemente a camada de Infraestrutura: implementacoes reais de Repository, configuracao do ORM
- Implemente a camada de Apresentacao: controller, tipos de requisicao e tipos de resposta
- Testes E2E
O segredo e escrever de dentro para fora. Comecar pelo desenho das tabelas do banco vai contra o espirito de DDD. O modelo de dominio vem primeiro; as tabelas, depois. Esse principio nao e negociavel.
Boas praticas para a estrutura de pastas
Esta e a primeira pergunta em que muita gente tropeΓ§a ao organizar um projeto DDD.
Vale mais dividir por camada, ou por dominio / funcionalidade?
A resposta curta e: a abordagem padrao e dominio primeiro, e depois as camadas dentro dele.
β Primeiro por camadas (tendendo a um antipadrao)
src/
βββ controllers/
β βββ user.controller.ts
β βββ order.controller.ts
β βββ product.controller.ts
βββ services/
β βββ user.service.ts
β βββ order.service.ts
β βββ product.service.ts
βββ repositories/
β βββ user.repository.ts
β βββ order.repository.ts
β βββ product.repository.ts
βββ entities/
βββ user.entity.ts
βββ order.entity.ts
βββ product.entity.ts
No comeco, isso e facil de entender, mas conforme o sistema cresce:
- Mudar um unico caso de uso passa a exigir navegar por quatro pastas
- Os limites do dominio ficam invisiveis porque tudo parece plano
- Fica dificil extrair servicos depois se voce caminhar para microservicos
A dor cresce bem rapido.
β Primeiro por dominio (recomendado)
src/
βββ contexts/ β Organizado por Bounded Context
β βββ ordering/ β Contexto de pedidos
β β βββ domain/
β β β βββ order.ts (Entity / Aggregate Root)
β β β βββ order-item.ts
β β β βββ order-id.ts (Value Object)
β β β βββ order-status.ts
β β β βββ order.repository.ts(interface only)
β β β βββ events/
β β β βββ order-placed.ts
β β βββ application/
β β β βββ place-order.usecase.ts
β β β βββ cancel-order.usecase.ts
β β βββ infrastructure/
β β β βββ postgres-order.repository.ts
β β β βββ stripe-payment.gateway.ts
β β βββ presentation/
β β βββ order.controller.ts
β β βββ dto/
β β βββ place-order.request.ts
β β βββ order.response.ts
β βββ catalog/ β Contexto de catalogo de produtos
β β βββ (mesma estrutura)
β βββ shipping/ β Contexto de entrega
β βββ (mesma estrutura)
βββ shared-kernel/ β Value Objects compartilhados entre contextos
β βββ money.ts
β βββ email.ts
β βββ user-id.ts
βββ shared/ β Base tecnica transversal
βββ logger.ts
βββ transaction-manager.ts
βββ event-bus.ts
Esta organizacao tem tres vantagens importantes.
- Os limites do contexto ficam fisicamente visiveis. Surge naturalmente um design autocontido dentro de
ordering/. - Extrair para microservicos fica mais facil. Voce consegue puxar
contexts/ordering/para outro repositorio quase como esta. - Code review fica mais focado. Da para perceber de imediato se uma mudanca permanece dentro de
ordering.
Organizacao dentro da camada de Dominio
Quando domain/ comeca a crescer demais, uma boa abordagem e dividi-lo por aggregate.
ordering/domain/
βββ order/
β βββ order.ts (aggregate root)
β βββ order-item.ts
β βββ order-id.ts
β βββ order-status.ts
β βββ order.repository.ts
β βββ events/
β βββ order-placed.ts
β βββ order-cancelled.ts
βββ customer/
β βββ customer.ts
β βββ customer.repository.ts
βββ services/
βββ pricing.service.ts (Domain Service)
Quando um aggregate esta organizado como se fosse dono do proprio pequeno mundo, suas dependencias tambem ficam muito mais faceis de acompanhar.
Estrutura em monorepo
Quando varios servicos sao gerenciados em um unico repositorio, a estrutura fica mais ou menos assim.
monorepo/
βββ apps/
β βββ web/ (frontend em Next.js)
β βββ api/ (API backend)
β βββ admin/ (UI administrativa)
βββ packages/
β βββ ordering-context/ β Bounded Context empacotado como unidade
β βββ catalog-context/
β βββ shipping-context/
β βββ shared-kernel/
βββ package.json
O ponto principal e que cada Bounded Context vira um pacote npm independente. apps/api so os importa e usa, o que permite impor dependencias entre contextos no nivel do pacote.
Dicas de convencao de nomes
Ajuda manter consistencia em nomes de arquivos e classes. Estas regras costumam funcionar bem na pratica.
| Alvo | Exemplo |
|---|---|
| Entity / Aggregate Root | Order (substantivo, singular) |
| Value Object | Email, Money, OrderId |
| Repository (interface) | OrderRepository (termina com Repository) |
| Repository (implementation) | PostgresOrderRepository (tecnologia + Repository) |
| Domain Service | PricingService, TransferService |
| Application Service | PlaceOrderUseCase, CancelOrderUseCase |
| Domain Event | OrderPlaced, OrderCancelled (passado) |
| Command (DTO) | PlaceOrderCommand |
Em especial, colocar Domain Events no passado e importante. Isso deixa claro: "isto e algo que aconteceu", e nao "algo que estamos prestes a fazer".
Camadas em sistemas com UI
Ate aqui a discussao esteve mais voltada para backend. Mas o que muda quando entra uma UI (frontend web)?
Se voce levar as quatro camadas do backend do mesmo jeito para o frontend...
A conclusao e simples: geralmente e melhor nao forcar essa estrutura de forma direta. O frontend tem preocupacoes proprias, e obrigar o mesmo modelo do servidor deixa o design apertado.
[Preocupacoes proprias do frontend]
- Routing
- Estado de tela (loading, erro, itens selecionados etc.)
- Validacao de formularios (para UX)
- Animacao
- Re-renderizacao reativa
Se voce colocar tudo isso dentro da camada de Dominio, o Dominio passa a depender do framework, como React ou Vue, e o objetivo original se perde.
Um modelo de camadas para frontend
Uma separacao pratica se parece com esta.
flowchart TB
R["Camada Pages / Routes<br/>Next.js app/, pages/, etc."]
C["Camada Components<br/>UI e preocupacoes visuais"]
V["Camada ViewModel / Hooks<br/>estado e chamadas aos casos de uso"]
A["Camada Application<br/>casos de uso / clients de API"]
D["Camada Domain<br/>modelo minimo de dominio no front"]
R --> C --> V --> A --> D
classDef layer fill:#eef2ff,stroke:#6366f1,color:#312e81
classDef domain fill:#c7d2fe,stroke:#4338ca,color:#1e1b4b
class R,C,V,A layer
class D domainAs responsabilidades de cada camada ficam assim.
| Camada | Responsabilidade | Exemplo |
|---|---|---|
| Pages / Routes | Mapear URLs para paginas | Pagina /orders/new |
| Components | Visual e interacoes | <OrderForm />, <Button /> |
| ViewModel / Hooks | Gerenciar estado de tela e disparar casos de uso | useOrderForm() |
| Application | Chamar APIs e dar forma aos dados | placeOrderUseCase() |
| Domain | Regras minimas de dominio necessarias no frontend | Order.canCancel() |
Exemplo de implementacao em React + TypeScript
// Camada de Dominio: mantenha uma versao enxuta das regras do backend.
class Order {
constructor(
public readonly id: string,
public readonly status: OrderStatus,
public readonly items: OrderItem[],
) {}
// Coloque aqui a decisao de se o botao Cancelar pode ser clicado.
canCancel(): boolean {
return this.status === "PENDING" || this.status === "CONFIRMED";
}
totalPrice(): number {
return this.items.reduce((s, i) => s + i.price * i.quantity, 0);
}
}
// Camada de Aplicacao: um caso de uso que chama a API.
class PlaceOrderUseCase {
constructor(private readonly api: OrderApiClient) {}
async execute(items: CartItem[]): Promise<Order> {
const response = await this.api.placeOrder({ items });
return Order.fromResponse(response);
}
}
// Camada ViewModel / Hooks: gerencia o estado da tela.
function useOrderForm() {
const [items, setItems] = useState<CartItem[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const placeOrder = useMemo(() => new PlaceOrderUseCase(orderApi), []);
const submit = async () => {
setIsSubmitting(true);
try {
await placeOrder.execute(items);
} catch (e) {
setError(formatError(e));
} finally {
setIsSubmitting(false);
}
};
return { items, setItems, isSubmitting, error, submit };
}
// Camada Components: foco apenas na apresentacao.
function OrderForm() {
const { items, isSubmitting, error, submit } = useOrderForm();
return (
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
{/* JSX */}
</form>
);
}
O ponto-chave e que o Component pode se concentrar apenas na apresentacao. O estado fica no Hook, regras de negocio como canCancel() ficam no Dominio e chamadas a API ficam na camada de Aplicacao.
Quanto de Domain o frontend deve ter?
Isso depende da natureza do projeto.
| Natureza do projeto | Camada Domain no frontend |
|---|---|
| Principalmente telas CRUD simples | Quase desnecessaria. Pode-se usar diretamente as respostas do API |
| Sao necessarios calculos ou decisoes complexas na UI | Tenha uma. Por exemplo, recalcular precos ou decidir o que pode ser selecionado |
| Suporte offline / mobile | Tenha uma boa (veja adiante) |
Outro ponto importante: nao ha necessidade de copiar exatamente o modelo de dominio do servidor. No frontend, mantenha apenas as regras relacionadas a interacao do usuario.
BFF como opcao
Se "o formato de dados do frontend nao combina com o backend" ou "uma tela precisa combinar varias APIs", entao colocar um BFF no meio costuma funcionar muito bem.
flowchart TB
Browser["Navegador"]
BFF["BFF<br/>Next.js API Routes / Hono, etc.<br/>Endpoint otimizado para a tela"]
Ordering["Servico de pedidos"]
Catalog["Servico de catalogo"]
Inventory["Servico de estoque"]
Response["Resposta moldada para a tela"]
Browser --> BFF
BFF --> Ordering
BFF --> Catalog
BFF --> Inventory
Ordering --> BFF
Catalog --> BFF
Inventory --> BFF
BFF --> Response --> Browser
classDef layer fill:#eef2ff,stroke:#6366f1,color:#312e81
classDef service fill:#e0e7ff,stroke:#4f46e5,color:#312e81
classDef output fill:#ffffff,stroke:#94a3b8,color:#334155
class Browser,BFF layer
class Ordering,Catalog,Inventory service
class Response outputUm BFF e uma camada fina voltada apenas para a tela, sem dominio proprio. Ele pode ler atraves de varios aggregates de uma vez ou combinar resultados de varios contextos em um unico JSON moldado para a UI. Tambem e natural faze-lo atuar como o lado de consulta do CQRS.
DDD pode ser aplicado a apps moveis?
"DDD nao e algo mais do lado do servidor?"
Essa e uma impressao comum, mas DDD pode perfeitamente ser aplicado a apps moveis tambem. Na verdade, quando entram em cena suporte offline e sincronizacao, os beneficios de um modelo de dominio podem ser ainda maiores.
Realidades especificas do mobile
| Realidade | Impacto |
|---|---|
| Suporte offline | Um banco local e necessario -> Repository passa a ser valioso |
| Push notifications / trabalho em background | Combina bem com Domain Events |
| Sensores do dispositivo / camera | Trate como Infrastructure |
| Ciclo de vida do app | Torna a persistencia de estado mais complexa |
| Compatibilidade de versao | Compatibilidade com dispositivos antigos e necessaria |
Se tudo isso for escrito diretamente em Activity, Fragment ou ViewController, voce pode terminar com um sistema que desmorona na hora em que o SO muda.
Exemplo de estrutura em camadas para implementacoes nativas (Swift / Kotlin)
App/
βββ Domain/
β βββ Entities/
β β βββ Recipe.swift
β β βββ Ingredient.swift
β βββ ValueObjects/
β β βββ CookingTime.swift
β βββ Repositories/
β β βββ RecipeRepository.swift (protocol / interface)
βββ Application/
β βββ UseCases/
β βββ SearchRecipeUseCase.swift
βββ Infrastructure/
β βββ Repositories/
β β βββ CoreDataRecipeRepository.swift (banco de dados local)
β β βββ ApiRecipeRepository.swift (API remota)
β βββ Storage/
β β βββ KeyValueStore.swift
β βββ Sync/
β βββ RecipeSyncService.swift
βββ Presentation/
βββ ViewModels/
β βββ RecipeListViewModel.swift
βββ Views/
βββ RecipeListView.swift (SwiftUI)
Em linha com as secoes anteriores, o UseCase fica na camada de Aplicacao, enquanto a camada de Dominio continua focada no modelo e em suas regras.
Um exemplo em que Repository brilha no suporte offline
// Camada de Dominio: abstrai varios destinos de persistencia.
protocol RecipeRepository {
func find(id: RecipeId) async throws -> Recipe?
func save(_ recipe: Recipe) async throws
}
// Camada de Infraestrutura: combina local + remoto.
class HybridRecipeRepository: RecipeRepository {
let local: CoreDataRecipeRepository
let remote: ApiRecipeRepository
func find(id: RecipeId) async throws -> Recipe? {
// Pode ler do armazenamento local mesmo sem conexao.
if let cached = try await local.find(id: id) {
return cached
}
let remote = try await remote.find(id: id)
if let recipe = remote {
try await local.save(recipe) // atualizar o cache
}
return remote
}
}
Como a logica offline fica fechada dentro da implementacao do Repository, o ViewModel e o Dominio podem simplesmente dizer "busque isto". Esse e o poder do DDD.
Aplicando em React Native / Flutter
A mesma forma de pensar tambem funciona em desenvolvimento cross-platform. Na verdade, uma grande vantagem e que voce pode compartilhar uma camada de Dominio comum entre a aplicacao web e a aplicacao mobile.
shared/
βββ domain/ β Compartilhado entre web e mobile
βββ recipe.ts
βββ recipe.repository.ts
apps/
βββ web/
β βββ infrastructure/
β βββ api-recipe.repository.ts
βββ mobile/
βββ infrastructure/
βββ sqlite-recipe.repository.ts (offline first)
Essa estrutura e possivel justamente porque o Dominio nao depende de mais nada, o que e uma das expressoes mais fortes do que DDD faz bem.
Mas nao se esqueca de manter leve
Levar todo o conjunto de ferramentas de DDD do servidor para um app mobile e demais. Em apps moveis:
- Os limites de aggregate nao precisam ser tao rigidos quanto no servidor, porque no fim quem garante consistencia e o servidor
- Domain Events normalmente so precisam ser notificacoes locais de eventos
- Em muitos casos, ir ate CQRS completo e desnecessario
Um equilibrio realista e manter uma versao enxuta, no cliente, do modelo de dominio criado no servidor.
CRUD, telas administrativas e convivencia com DDD
Depois de ler ate aqui, muita gente provavelmente pensa: "Grande parte do nosso sistema e tela de configuracao e tela administrativa. Isso ainda e DDD?" Esse desconforto esta correto.
DDD e para dominios complexos. Se voce leva isso para lugares sem complexidade, so deixa o codigo maior sem ganhar nada.
Avalie o "peso" do dominio
Mesmo dentro de um unico projeto, a complexidade varia por funcionalidade.
| Funcionalidade | Onde esta a complexidade | Abordagem de implementacao |
|---|---|---|
| Processamento de pedidos | alocacao, calculo de precos, transicoes de estado complexas | Investir com DDD |
| Gestao de estoque | reserva, liberacao, manutencao de consistencia | Investir com DDD |
| Cadastro de produtos | majoritariamente CRUD | Uma abordagem leve basta |
| Configuracao de usuario | simples armazenamento de valores | CRUD e suficiente |
| Visualizacao de logs administrativos | apenas consultar e exibir | CRUD e suficiente |
O que importa aqui e que a classificacao anterior entre core, supporting e generic corresponde diretamente ao peso da implementacao.
| Categoria de dominio | Estilo de implementacao | Exemplos |
|---|---|---|
| Core Domain | DDD completo | pedidos, estoque, estrategia de precos |
| Supporting Subdomain | DDD leve ou transaction script | cadastro de produtos, configuracoes de entrega |
| Generic Subdomain | CRUD ou SaaS | autenticacao, envio de email, telas de configuracao |
Como escrever as partes CRUD
Para areas mais leves, esta perfeitamente ok escrever transaction scripts simples.
// β
Isto e suficiente para atualizar uma tela de configuracao.
class UpdateSiteSettingsHandler {
constructor(private readonly db: Database) {}
async execute(input: UpdateSiteSettingsInput): Promise<void> {
await this.db.siteSettings.update({
where: { id: 1 },
data: input,
});
}
}
Se voce forcar Entities, Repositories e Domain Services aqui, so aumenta a quantidade de codigo sem ganhar nada em troca.
Misturando os dois no mesmo projeto
Na pratica, a resposta realista e mudar o estilo de implementacao conforme o contexto.
src/contexts/
βββ ordering/ β DDD completo
β βββ domain/
β βββ application/
β βββ infrastructure/
β βββ presentation/
βββ inventory/ β DDD completo
β βββ (mesma coisa)
βββ catalog/ β DDD leve (manter Entities, minimizar Services)
β βββ (versao simplificada)
βββ admin/ β Baseado em CRUD
βββ handlers/ (handlers finos apenas)
βββ controllers/
Desde que cada contexto seja internamente consistente, isso basta. Nao ha necessidade de impor um unico estilo para o projeto inteiro.
CQRS ajuda em APIs administrativas de leitura
Um caso comum em telas administrativas e: "Quero juntar dados de varios aggregates e mostrar uma lista". Isso normalmente significa ler atraves de varios aggregates, o que combina mal com as regras estritas de aggregate em DDD.
E ai que um uso leve de CQRS ajuda: seja rigoroso nas escritas atraves de aggregates e permita SQL puro nas leituras.
// Escritas passam por aggregates (rigoroso).
class CancelOrderUseCase {
async execute(orderId: OrderId): Promise<void> {
const order = await this.orderRepository.findById(orderId);
order.cancel(); // regra de dominio
await this.orderRepository.save(order);
}
}
// Consultas de leitura podem usar SQL puro (otimizado para a tela).
class OrderListQuery {
async findForAdmin(filter: AdminFilter): Promise<OrderListView[]> {
return this.db.raw(`
SELECT o.id, o.status, u.name AS user_name, p.name AS product_name, ...
FROM orders o
JOIN users u ON ...
JOIN order_items oi ON ...
WHERE ...
`);
}
}
Nao recupere aggregates um por um para depois fazer join em memoria. Para leitura, use ferramentas de leitura. Esse e o caminho pragmatico.
Principios para misturar CRUD e DDD
Por fim, estes sao os principios para misturar os dois.
- Desenhe bem os limites de contexto. Mesmo em areas nao DDD, mantenha a ideia de limites.
- Nao economize no core domain. Se voce cortar caminho ali, vai se arrepender depois.
- Escreva as areas CRUD de forma simples. Nao force DDD onde nao precisa.
- Permita uma rota separada para leituras. Use read models ou CQRS leve.
- Promova depois se a complexidade crescer. Se algo que comecou como CRUD ficar complexo, mova para DDD naquele momento.
DDD nao e religiao; e ferramenta. Invista na proporcao da complexidade do dominio.
Armadilhas comuns
1. Anemic domain model
Este e o estado em que uma Entity vira pouco mais que um saco de getters e setters, e a logica vaza para Services. Guarde isto: um objeto sem comportamento nao e, de verdade, um objeto.
2. Aplicar DDD a tudo
Telas de configuracao, telas administrativas, CRUD simples. Se voce levar DDD para tudo isso, a velocidade de desenvolvimento vai cair. Use-o apenas onde existe dominio complexo. A secao anterior sobre CRUD, telas administrativas e convivencia com DDD tambem cobre esse ponto.
3. Ficar apenas com os padroes taticos
Criar Entities e Repositories nao faz algo ser DDD. Sem a discussao sobre ubiquitous language e bounded contexts, os padroes taticos entregam pouco valor.
Passos para comecar com DDD
Se voce quer introduzir DDD na pratica, recomendo fazer isso gradualmente em vez de pular direto para o pacote completo.
- Colete a linguagem do negocio criando um glossario junto com especialistas de dominio
- Esteja consciente dos limites e desenhe linhas em torno do que pertence ao mesmo contexto
- Comece com Value Objects e substitua tipos primitivos por tipos significativos
- Identifique aggregates com cuidado e pense em termos de limites de transacao
- Use eventos para desacoplar quando fizer sentido, introduzindo Domain Events
Encerrando
Pessoalmente, acho que a essencia de DDD e encarar de frente, no codigo, a complexidade do dominio.
Se decorar padroes vira o objetivo, o codigo costuma ficar mais complexo, e nao menos. Fazer a ubiquitous language crescer como equipe e ao mesmo tempo o ponto de partida e, em certo sentido, o destino de DDD.
Que tal comecar o code review de amanha com uma pergunta simples: "Esse nome de classe tambem faria sentido para as pessoas do negocio?"
Referencias
- Eric Evans "Domain-Driven Design: Tackling Complexity in the Heart of Software"
- Eric Evans "DDD Reference: Definitions and Pattern Summaries" https://www.domainlanguage.com/ddd/reference/
- Vaughn Vernon "Implementing Domain-Driven Design"
- Masanobu Naruse "Introducao a Domain-Driven Design"
- Toru Masuda "Principios de design de sistemas uteis no trabalho real"
- Martin Fowler "Patterns of Enterprise Application Architecture"
- Robert C. Martin "Clean Architecture"
- Alberto Brandolini "Introducing EventStorming"
