Domain-Driven Design (DDD)
🎯 O que é Domain-Driven Design?
Domain-Driven Design (DDD) é uma abordagem filosófica e metodológica para o desenvolvimento de software que coloca o domínio de negócio no centro absoluto de todas as decisões de design e arquitetura. Criado por Eric Evans em 2003 através do livro seminal "Domain-Driven Design: Tackling Complexity in the Heart of Software", o DDD representa uma mudança paradigmática na forma como pensamos sobre desenvolvimento de software.
Definição Profunda
DDD não é apenas uma técnica de programação, mas uma estratégia de modelagem que busca:
- Capturar a Essência do Negócio: O software deve ser um reflexo fiel da realidade do negócio, não uma representação técnica distorcida
- Criar Linguagem Compartilhada: Desenvolvedores e especialistas de domínio devem falar a mesma língua
- Gerenciar Complexidade: Dividir sistemas complexos em partes menores e mais gerenciáveis
- Evoluir com o Negócio: O software deve evoluir naturalmente conforme o domínio evolui
Princípios Fundamentais
1. Domínio como Centro de Gravidade
- Toda decisão técnica deve ser subordinada às necessidades do domínio
- A arquitetura serve o domínio, não o contrário
- O código deve "falar" a linguagem do negócio
2. Modelagem Colaborativa
- Desenvolvedores e especialistas de domínio trabalham juntos
- O conhecimento do domínio é compartilhado e refinado continuamente
- A modelagem é um processo iterativo e colaborativo
3. Linguagem Ubíqua
- Uma linguagem única e consistente usada por todos
- Elimina a tradução entre linguagem técnica e de negócio
- Reflete-se diretamente no código e documentação
4. Contextos Delimitados
- Divide o sistema em áreas de responsabilidade bem definidas
- Cada contexto tem suas próprias regras e linguagem
- Reduz a complexidade através da separação de conceitos
DDD vs Outras Abordagens
| Aspecto | DDD | Anêmico | CRUD | SOA |
|---|---|---|---|---|
| Foco | Domínio | Dados | Operações | Serviços |
| Lógica | Entidades | Serviços | Controllers | Services |
| Linguagem | Ubíqua | Técnica | Técnica | Técnica |
| Complexidade | Gerenciada | Espalhada | Simples | Distribuída |
| Manutenibilidade | Alta | Baixa | Média | Média |
🚀 Motivo da Existência
O DDD surgiu como resposta a problemas crônicos que afligem o desenvolvimento de software há décadas. Eric Evans identificou que a maioria dos projetos falha não por problemas técnicos, mas por problemas de comunicação e modelagem.
Problemas Fundamentais que o DDD Resolve
1. Crise de Comunicação entre Mundos Diferentes
Problema Real:
- Desenvolvedores falam em "tabelas", "APIs", "endpoints"
- Especialistas de domínio falam em "pedidos", "clientes", "estoque"
- Resultado: Tradução constante que gera erros e mal-entendidos
Como o DDD Resolve:
- Cria uma linguagem ubíqua que ambos os lados usam
- Elimina a necessidade de tradução
- O código fala diretamente a linguagem do negócio
// ❌ Antes - Tradução constante
public class OrderController
{
public void ProcessOrder(OrderDTO orderData) // Tradução: Order -> OrderDTO
{
var orderEntity = MapToEntity(orderData); // Tradução: DTO -> Entity
_orderService.Save(orderEntity); // Tradução: Save -> Process
}
}
// ✅ Depois - Linguagem ubíqua
public class OrderProcessor
{
public void FulfillOrder(Order order) // Linguagem direta do domínio
{
order.Confirm(); // Método que faz sentido no domínio
_orderRepository.Save(order);
}
}2. Complexidade Técnica Ocultando Complexidade Real
Problema Real:
- Desenvolvedores focam em "como implementar" em vez de "o que implementar"
- A complexidade técnica mascara a complexidade do domínio
- Resultado: Soluções técnicas elegantes para problemas errados
Como o DDD Resolve:
- Força o foco na complexidade do domínio
- A complexidade técnica serve ao domínio, não o contrário
- Separa claramente o que é regra de negócio do que é detalhe técnico
3. Modelos Anêmicos e Lógica Espalhada
Problema Real:
- Classes são apenas containers de dados (anêmicas)
- Lógica de negócio fica espalhada em serviços, controllers, etc.
- Resultado: Violação do princípio de responsabilidade única
Como o DDD Resolve:
- Encapsula lógica de negócio nas entidades de domínio
- Cada entidade é responsável por suas próprias regras
- Elimina a necessidade de serviços que "orquestram" dados
// ❌ Modelo Anêmico
public class Order
{
public Guid Id { get; set; }
public string Status { get; set; }
public List<OrderItem> Items { get; set; }
}
public class OrderService
{
public void AddItem(Order order, Product product, int quantity)
{
// Lógica de negócio espalhada no serviço
if (order.Status != "Draft")
throw new Exception("Cannot add items to confirmed order");
var item = new OrderItem { ProductId = product.Id, Quantity = quantity };
order.Items.Add(item);
}
}
// ✅ Modelo Rico
public class Order : AggregateRoot<OrderId>
{
public OrderId Id { get; private set; }
public OrderStatus Status { get; private set; }
public List<OrderLine> Lines { get; private set; }
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
// Lógica de negócio encapsulada na entidade
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to confirmed order");
var line = new OrderLine(productId, quantity, unitPrice);
Lines.Add(line);
RecalculateTotal();
}
}4. Mudanças de Requisitos como Pesadelo
Problema Real:
- Mudanças de negócio requerem mudanças em múltiplas camadas
- Código fortemente acoplado dificulta evolução
- Resultado: Custo alto de manutenção e evolução
Como o DDD Resolve:
- Isola mudanças em contextos específicos
- Mudanças de domínio afetam apenas o domínio
- Facilita refatoração e evolução
5. Escalabilidade e Manutenibilidade Comprometidas
Problema Real:
- Código monolítico e difícil de entender
- Dependências circulares e acoplamento alto
- Resultado: Equipes grandes não conseguem trabalhar eficientemente
Como o DDD Resolve:
- Divide o sistema em contextos bem definidos
- Cada contexto pode evoluir independentemente
- Facilita trabalho em paralelo de múltiplas equipes
Impacto Real dos Problemas
| Problema | Impacto | Custo |
|---|---|---|
| Comunicação Ruim | 40% dos bugs | $50k-500k por projeto |
| Modelos Anêmicos | 60% mais tempo de desenvolvimento | $100k-1M por projeto |
| Mudanças Difíceis | 80% do tempo em manutenção | $200k-2M por projeto |
| Complexidade Técnica | 50% de features desnecessárias | $75k-750k por projeto |
Por que DDD é a Solução
- Reduz Custo de Comunicação: Linguagem ubíqua elimina traduções
- Aumenta Velocidade: Foco no que importa (domínio)
- Melhora Qualidade: Lógica centralizada e testável
- Facilita Mudanças: Isolamento de responsabilidades
- Permite Escalabilidade: Contextos independentes
🏗️ Conceitos Principais do DDD
1. Ubiquitous Language (Linguagem Ubíqua)
A Linguagem Ubíqua é o coração do DDD e representa uma revolução na comunicação entre desenvolvedores e especialistas de domínio. Não é apenas um vocabulário compartilhado, mas uma transformação cultural na forma como equipes se comunicam.
Definição Profunda
A Linguagem Ubíqua é um vocabulário estruturado e consistente que:
- É usado por todos os membros da equipe (desenvolvedores, analistas, especialistas de domínio)
- Reflete-se diretamente no código (nomes de classes, métodos, propriedades)
- Evolui com o domínio através de discussões e refinamentos contínuos
- Elimina ambiguidades e mal-entendidos
Por que é Crítica?
Problema Real:
Especialista de Domínio: "Quando um cliente faz um pedido, precisamos reservar o estoque"
Desenvolvedor: "Entendi, vou criar um endpoint POST /api/orders que chama o service.ReserveInventory()"Resultado: Tradução constante que gera erros e perda de contexto.
Solução com Linguagem Ubíqua:
Ambos: "Quando um cliente faz um pedido, precisamos reservar o estoque"
Código: order.ReserveStock() // Linguagem direta do domínioImplementação Prática
1. Nomenclatura Consistente
// ❌ Inconsistente - Mistura linguagens
public class OrderController
{
public void ProcessOrder(OrderDTO orderData) // "Process" é técnico
{
var orderEntity = MapToEntity(orderData); // "Entity" é técnico
_orderService.Save(orderEntity); // "Save" é técnico
}
}
// ✅ Consistente - Linguagem ubíqua
public class OrderProcessor
{
public void FulfillOrder(Order order) // "Fulfill" é do domínio
{
order.Confirm(); // "Confirm" é do domínio
_orderRepository.Persist(order); // "Persist" é do domínio
}
}2. Documentação Alinhada
/// <summary>
/// Confirma um pedido, reservando estoque e gerando fatura
/// </summary>
/// <remarks>
/// Um pedido confirmado não pode ser modificado e deve ter pelo menos um item.
/// A confirmação dispara eventos de domínio para reserva de estoque e geração de fatura.
/// </remarks>
public void Confirm()
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Apenas pedidos em rascunho podem ser confirmados");
if (Lines.Count == 0)
throw new InvalidOperationException("Pedido deve ter pelo menos um item para ser confirmado");
Status = OrderStatus.Confirmed;
ConfirmedAt = DateTime.UtcNow;
AddDomainEvent(new OrderConfirmedEvent(Id));
}3. Glossário de Domínio
/// <summary>
/// Glossário de Domínio - Gestão de Pedidos
///
/// Pedido (Order): Representa uma solicitação de compra feita por um cliente
/// - Rascunho (Draft): Pedido em criação, pode ser modificado
/// - Confirmado (Confirmed): Pedido validado, estoque reservado
/// - Cancelado (Cancelled): Pedido cancelado pelo cliente ou sistema
/// - Enviado (Shipped): Pedido enviado para o cliente
/// - Entregue (Delivered): Pedido entregue ao cliente
///
/// Item de Pedido (OrderLine): Produto específico em um pedido
/// - Quantidade: Número de unidades do produto
/// - Preço Unitário: Preço por unidade no momento do pedido
/// - Total: Quantidade × Preço Unitário
///
/// Confirmação: Processo de validar e finalizar um pedido
/// - Reserva estoque automaticamente
/// - Gera fatura
/// - Dispara eventos para outros contextos
/// </summary>4. Eventos de Domínio com Linguagem Clara
// ❌ Eventos técnicos
public class OrderSavedEvent : DomainEvent
{
public Guid OrderId { get; set; }
}
// ✅ Eventos com linguagem ubíqua
public class OrderConfirmedEvent : DomainEvent
{
public OrderId OrderId { get; }
public DateTime ConfirmedAt { get; }
public OrderConfirmedEvent(OrderId orderId)
{
OrderId = orderId;
ConfirmedAt = DateTime.UtcNow;
}
}Benefícios da Linguagem Ubíqua
- Reduz Erros de Comunicação: 60% menos bugs relacionados a mal-entendidos
- Acelera Desenvolvimento: Elimina tempo gasto em traduções
- Melhora Manutenibilidade: Código auto-documentado
- Facilita Onboarding: Novos desenvolvedores entendem o domínio rapidamente
- Alinha Equipes: Todos falam a mesma língua
Desafios e Soluções
Desafio 1: Resistência à Mudança
- Solução: Workshops regulares de refinamento da linguagem
Desafio 2: Linguagem Técnica Persistente
- Solução: Code reviews focados em linguagem ubíqua
Desafio 3: Evolução da Linguagem
- Solução: Documentação viva e atualizada constantemente
2. Bounded Context (Contexto Delimitado)
O Bounded Context é uma das estratégias mais poderosas do DDD para gerenciar complexidade. É a técnica de dividir um sistema grande e complexo em áreas de responsabilidade bem definidas, cada uma com seu próprio modelo, linguagem e regras.
Definição Profunda
Um Bounded Context é uma fronteira conceitual onde:
- Um modelo específico se aplica de forma consistente
- Uma linguagem ubíqua específica é usada
- Regras de negócio específicas são aplicadas
- Conceitos têm significados específicos dentro desse contexto
Por que Bounded Contexts são Essenciais?
Problema Real:
Sistema monolítico com 50+ entidades
- Customer significa "cliente que compra" em alguns lugares
- Customer significa "usuário do sistema" em outros lugares
- Customer significa "conta bancária" em outros lugaresResultado: Confusão, bugs e dificuldade de manutenção.
Solução com Bounded Contexts:
Contexto: Gestão de Pedidos
- Customer = "Cliente que faz pedidos"
- Regras: Validação de crédito, histórico de compras
Contexto: Gestão de Usuários
- Customer = "Usuário do sistema"
- Regras: Autenticação, permissões, perfis
Contexto: Gestão Financeira
- Customer = "Conta bancária"
- Regras: Saldo, transações, limitesImplementação Prática
1. Identificação de Contextos
/// <summary>
/// Contextos identificados para o sistema de E-commerce
///
/// 1. OrderManagement (Gestão de Pedidos)
/// - Responsabilidade: Criação, modificação e acompanhamento de pedidos
/// - Conceitos: Order, OrderLine, OrderStatus, ShippingAddress
/// - Regras: Validação de estoque, cálculo de preços, status de pedido
///
/// 2. CustomerManagement (Gestão de Clientes)
/// - Responsabilidade: Cadastro e gestão de clientes
/// - Conceitos: Customer, CustomerProfile, CustomerStatus, Address
/// - Regras: Validação de dados, segmentação, histórico
///
/// 3. ProductCatalog (Catálogo de Produtos)
/// - Responsabilidade: Gestão de produtos e categorias
/// - Conceitos: Product, Category, ProductVariant, Pricing
/// - Regras: Validação de produtos, hierarquia de categorias
///
/// 4. Inventory (Gestão de Estoque)
/// - Responsabilidade: Controle de estoque e disponibilidade
/// - Conceitos: StockItem, Warehouse, Reservation, StockMovement
/// - Regras: Reserva de estoque, movimentação, alertas
///
/// 5. Payment (Processamento de Pagamentos)
/// - Responsabilidade: Processamento e gestão de pagamentos
/// - Conceitos: Payment, PaymentMethod, Transaction, Refund
/// - Regras: Validação de pagamento, processamento, reembolsos
///
/// 6. Shipping (Logística e Entrega)
/// - Responsabilidade: Gestão de entregas e logística
/// - Conceitos: Shipment, ShippingMethod, Tracking, Delivery
/// - Regras: Cálculo de frete, rastreamento, confirmação
/// </summary>2. Estrutura de Namespaces
// Contexto: Gestão de Pedidos
namespace OrderManagement.Domain.Entities
{
public class Order : AggregateRoot<OrderId>
{
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public List<OrderLine> Lines { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
public ShippingAddress ShippingAddress { get; private set; }
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
// Regras específicas do contexto de pedidos
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to confirmed order");
var line = new OrderLine(productId, quantity, unitPrice);
Lines.Add(line);
RecalculateTotal();
}
public void Confirm()
{
// Regras de confirmação específicas do contexto de pedidos
if (Lines.Count == 0)
throw new InvalidOperationException("Order must have at least one item");
if (ShippingAddress == null)
throw new InvalidOperationException("Shipping address is required");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
}
}
}
// Contexto: Gestão de Clientes
namespace CustomerManagement.Domain.Entities
{
public class Customer : AggregateRoot<CustomerId>
{
public CustomerId Id { get; private set; }
public string Name { get; private set; }
public Email Email { get; private set; }
public CustomerStatus Status { get; private set; }
public CustomerProfile Profile { get; private set; }
public List<Address> Addresses { get; private set; }
public void UpdateProfile(string name, Email email)
{
// Regras específicas do contexto de clientes
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name cannot be empty");
Name = name;
Email = email;
AddDomainEvent(new CustomerProfileUpdatedEvent(Id, Name, Email));
}
public void AddAddress(Address address)
{
// Regras específicas do contexto de clientes
if (address == null)
throw new ArgumentException("Address cannot be null");
if (Addresses.Count >= 5)
throw new InvalidOperationException("Maximum 5 addresses allowed");
Addresses.Add(address);
}
}
}3. Mapeamento entre Contextos
/// <summary>
/// Mapeamento entre contextos - Context Mapping
///
/// 1. Customer (CustomerManagement) -> CustomerReference (OrderManagement)
/// - Tipo: Shared Kernel
/// - Propósito: Referência a cliente em pedidos
/// - Dados: Apenas Id e Name (dados mínimos necessários)
///
/// 2. Product (ProductCatalog) -> ProductReference (OrderManagement)
/// - Tipo: Published Language
/// - Propósito: Referência a produto em pedidos
/// - Dados: Id, Name, Price (snapshot no momento do pedido)
///
/// 3. Order (OrderManagement) -> OrderSummary (CustomerManagement)
/// - Tipo: Customer-Supplier
/// - Propósito: Histórico de pedidos do cliente
/// - Dados: Id, TotalAmount, Status, CreatedAt
///
/// 4. Payment (Payment) -> PaymentInfo (OrderManagement)
/// - Tipo: Conformist
/// - Propósito: Informações de pagamento do pedido
/// - Dados: PaymentId, Amount, Status, Method
/// </summary>
// Exemplo de mapeamento
namespace OrderManagement.Domain.ValueObjects
{
public class CustomerReference : ValueObject
{
public CustomerId Id { get; }
public string Name { get; }
public CustomerReference(CustomerId id, string name)
{
Id = id;
Name = name;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Id;
yield return Name;
}
}
}4. Estratégias de Integração
/// <summary>
/// Estratégias de integração entre contextos
///
/// 1. Shared Kernel (Núcleo Compartilhado)
/// - Contextos compartilham um modelo comum
/// - Usado para conceitos que são realmente compartilhados
/// - Exemplo: Money, Email, Address
///
/// 2. Customer-Supplier (Cliente-Fornecedor)
/// - Um contexto fornece dados para outro
/// - Relação direta com dependência clara
/// - Exemplo: ProductCatalog fornece dados para OrderManagement
///
/// 3. Conformist (Conformista)
/// - Um contexto se adapta ao modelo de outro
/// - Usado quando não há controle sobre o contexto fornecedor
/// - Exemplo: Integração com sistemas externos
///
/// 4. Anti-Corruption Layer (Camada Anticorrupção)
/// - Camada que traduz entre modelos diferentes
/// - Protege o contexto interno de influências externas
/// - Exemplo: Integração com APIs de terceiros
///
/// 5. Published Language (Linguagem Publicada)
/// - Linguagem comum para comunicação entre contextos
/// - Usado para integração assíncrona
/// - Exemplo: Eventos de domínio publicados
///
/// 6. Open Host Service (Serviço de Hospedagem Aberta)
/// - API pública para outros contextos consumirem
/// - Contrato estável e bem documentado
/// - Exemplo: APIs REST para integração
/// </summary>Benefícios dos Bounded Contexts
- Reduz Complexidade: Divide problemas grandes em problemas menores
- Facilita Manutenção: Mudanças isoladas em contextos específicos
- Permite Evolução Independente: Cada contexto pode evoluir separadamente
- Melhora Performance: Otimizações específicas por contexto
- Facilita Trabalho em Equipe: Equipes podem trabalhar em contextos diferentes
Desafios e Soluções
Desafio 1: Identificar os Contextos Certos
- Solução: Event Storming e workshops com especialistas de domínio
Desafio 2: Gerenciar Integrações
- Solução: Context Mapping bem definido e documentado
Desafio 3: Evitar Over-Engineering
- Solução: Começar com poucos contextos e evoluir gradualmente
3. Entities (Entidades)
As Entidades são o coração do modelo de domínio e representam objetos que têm identidade única e podem evoluir ao longo do tempo. Diferentemente dos Value Objects, as entidades são definidas por sua identidade, não por seus atributos.
Definição Profunda
Uma Entidade é um objeto que:
- Mantém identidade única ao longo de sua vida útil
- Pode mudar seus atributos sem perder sua identidade
- Representa um conceito do domínio com continuidade temporal
- Encapsula lógica de negócio relacionada a sua identidade
Por que Entidades são Fundamentais?
Problema Real:
// ❌ Abordagem procedural - dados espalhados
public class OrderService
{
public void UpdateOrderStatus(Guid orderId, string newStatus)
{
var order = _repository.GetById(orderId);
order.Status = newStatus; // Mudança direta de dados
if (newStatus == "Confirmed")
{
// Lógica de negócio espalhada
ReserveInventory(order);
SendNotification(order);
UpdateCustomerHistory(order);
}
}
}Resultado: Lógica de negócio espalhada, violação de encapsulamento, difícil manutenção.
Solução com Entidades:
// ✅ Abordagem orientada a domínio
public class Order : Entity<OrderId>
{
public void Confirm()
{
// Lógica de negócio encapsulada na entidade
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot confirm non-draft order");
if (Lines.Count == 0)
throw new InvalidOperationException("Cannot confirm empty order");
Status = OrderStatus.Confirmed;
ConfirmedAt = DateTime.UtcNow;
// Eventos de domínio para coordenação
AddDomainEvent(new OrderConfirmedEvent(Id));
}
}Características Essenciais das Entidades
1. Identidade Única e Imutável
public class OrderId : ValueObject
{
public Guid Value { get; }
public OrderId(Guid value)
{
Value = value;
}
public static OrderId New() => new(Guid.NewGuid());
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}
public class Order : Entity<OrderId>
{
public OrderId Id { get; private set; } // Imutável após criação
public Order(OrderId id, CustomerId customerId)
{
Id = id; // Definido uma única vez
CustomerId = customerId;
Lines = new List<OrderLine>();
Status = OrderStatus.Draft;
}
}2. Encapsulamento de Estado
public class Order : Entity<OrderId>
{
// Estado privado - só pode ser modificado através de métodos
private readonly List<OrderLine> _lines;
private OrderStatus _status;
private Money _totalAmount;
// Propriedades públicas de leitura
public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
public OrderStatus Status => _status;
public Money TotalAmount => _totalAmount;
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
// Validação de regras de negócio
if (_status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to confirmed order");
// Modificação controlada do estado
var line = new OrderLine(productId, quantity, unitPrice);
_lines.Add(line);
RecalculateTotal();
}
private void RecalculateTotal()
{
_totalAmount = _lines.Aggregate(Money.Zero, (total, line) => total + line.Total);
}
}3. Lógica de Negócio Encapsulada
public class Order : Entity<OrderId>
{
public void Confirm()
{
// Invariantes de negócio
ValidateCanBeConfirmed();
// Mudança de estado
_status = OrderStatus.Confirmed;
ConfirmedAt = DateTime.UtcNow;
// Efeitos colaterais através de eventos
AddDomainEvent(new OrderConfirmedEvent(Id));
}
public void Cancel()
{
// Invariantes de negócio
if (_status == OrderStatus.Delivered)
throw new InvalidOperationException("Cannot cancel delivered order");
// Mudança de estado
_status = OrderStatus.Cancelled;
CancelledAt = DateTime.UtcNow;
// Efeitos colaterais
AddDomainEvent(new OrderCancelledEvent(Id));
}
private void ValidateCanBeConfirmed()
{
if (_status != OrderStatus.Draft)
throw new InvalidOperationException("Only draft orders can be confirmed");
if (_lines.Count == 0)
throw new InvalidOperationException("Order must have at least one item");
if (ShippingAddress == null)
throw new InvalidOperationException("Shipping address is required");
}
}4. Eventos de Domínio
public class Order : Entity<OrderId>
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
public void Confirm()
{
// ... lógica de confirmação ...
// Evento de domínio para coordenação
AddDomainEvent(new OrderConfirmedEvent(Id, CustomerId, TotalAmount));
}
}Padrões de Design para Entidades
1. Factory Pattern para Criação
public class OrderFactory
{
public static Order CreateDraft(CustomerId customerId)
{
var orderId = OrderId.New();
return new Order(orderId, customerId);
}
public static Order CreateFromExistingOrder(OrderId orderId, CustomerId customerId,
IEnumerable<OrderLine> lines, OrderStatus status)
{
var order = new Order(orderId, customerId);
foreach (var line in lines)
{
order.AddItem(line.ProductId, line.Quantity, line.UnitPrice);
}
if (status == OrderStatus.Confirmed)
order.Confirm();
return order;
}
}2. Specification Pattern para Consultas
public class OrderSpecification
{
public static Specification<Order> ByCustomer(CustomerId customerId)
{
return new Specification<Order>(order => order.CustomerId == customerId);
}
public static Specification<Order> ByStatus(OrderStatus status)
{
return new Specification<Order>(order => order.Status == status);
}
public static Specification<Order> ByDateRange(DateTime startDate, DateTime endDate)
{
return new Specification<Order>(order =>
order.CreatedAt >= startDate && order.CreatedAt <= endDate);
}
public static Specification<Order> HighValueOrders(Money minimumAmount)
{
return new Specification<Order>(order => order.TotalAmount >= minimumAmount);
}
}3. Repository Pattern para Persistência
public interface IOrderRepository
{
Task<Order> GetByIdAsync(OrderId id);
Task<IEnumerable<Order>> GetBySpecificationAsync(Specification<Order> spec);
Task AddAsync(Order order);
Task UpdateAsync(Order order);
Task DeleteAsync(OrderId id);
}
public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public async Task<Order> GetByIdAsync(OrderId id)
{
var orderEntity = await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id.Value);
return orderEntity != null ? MapToDomain(orderEntity) : null;
}
public async Task<IEnumerable<Order>> GetBySpecificationAsync(Specification<Order> spec)
{
var query = _context.Orders.Include(o => o.Lines);
var filteredQuery = spec.ToExpression().Compile();
var entities = await query.ToListAsync();
return entities.Select(MapToDomain).Where(filteredQuery);
}
}Boas Práticas para Entidades
1. Mantenha Entidades Pequenas e Focadas
// ❌ Entidade muito grande
public class Order : Entity<OrderId>
{
// 50+ propriedades e métodos
// Múltiplas responsabilidades
// Difícil de manter e testar
}
// ✅ Entidade focada
public class Order : Entity<OrderId>
{
// Apenas propriedades essenciais
// Métodos relacionados ao ciclo de vida do pedido
// Responsabilidade única
}2. Use Value Objects para Atributos Complexos
// ❌ Tipos primitivos
public class Order : Entity<OrderId>
{
public string CustomerName { get; set; }
public string CustomerEmail { get; set; }
public decimal TotalAmount { get; set; }
public string Currency { get; set; }
}
// ✅ Value Objects
public class Order : Entity<OrderId>
{
public CustomerReference Customer { get; private set; }
public Money TotalAmount { get; private set; }
public ShippingAddress ShippingAddress { get; private set; }
}3. Valide Invariantes de Negócio
public class Order : Entity<OrderId>
{
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
// Invariantes de negócio
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to confirmed order");
if (quantity.Value <= 0)
throw new ArgumentException("Quantity must be positive");
if (unitPrice.Amount <= 0)
throw new ArgumentException("Unit price must be positive");
// Lógica de negócio
var line = new OrderLine(productId, quantity, unitPrice);
_lines.Add(line);
RecalculateTotal();
}
}Benefícios das Entidades Bem Projetadas
- Encapsulamento: Lógica de negócio protegida e centralizada
- Manutenibilidade: Mudanças isoladas e previsíveis
- Testabilidade: Comportamento testável de forma isolada
- Consistência: Invariantes de negócio sempre respeitadas
- Legibilidade: Código que expressa intenção do domínio
public class Order : Entity<OrderId>
{
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public List<OrderLine> Lines { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
public Order(OrderId id, CustomerId customerId)
{
Id = id;
CustomerId = customerId;
Lines = new List<OrderLine>();
Status = OrderStatus.Draft;
TotalAmount = Money.Zero;
}
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to confirmed order");
var line = new OrderLine(productId, quantity, unitPrice);
Lines.Add(line);
RecalculateTotal();
}
public void Confirm()
{
if (Lines.Count == 0)
throw new InvalidOperationException("Cannot confirm empty order");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
}
private void RecalculateTotal()
{
TotalAmount = Lines.Aggregate(Money.Zero, (total, line) => total + line.Total);
}
}
public abstract class Entity<TId> where TId : ValueObject
{
public TId Id { get; protected set; }
protected Entity() { }
protected Entity(TId id)
{
Id = id;
}
public override bool Equals(object obj)
{
if (obj is not Entity<TId> other)
return false;
if (ReferenceEquals(this, other))
return true;
return Id.Equals(other.Id);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public static bool operator ==(Entity<TId> left, Entity<TId> right)
{
return EqualOperator(left, right);
}
public static bool operator !=(Entity<TId> left, Entity<TId> right)
{
return NotEqualOperator(left, right);
}
protected static bool EqualOperator(Entity<TId> left, Entity<TId> right)
{
if (left is null ^ right is null)
return false;
return left is null || left.Equals(right);
}
protected static bool NotEqualOperator(Entity<TId> left, Entity<TId> right)
{
return !EqualOperator(left, right);
}
}4. Value Objects (Objetos de Valor)
Value objects são objetos imutáveis que são definidos por seus atributos, não por identidade. Eles representam conceitos do domínio que não têm identidade própria.
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative");
if (string.IsNullOrEmpty(currency))
throw new ArgumentException("Currency cannot be empty");
Amount = amount;
Currency = currency.ToUpperInvariant();
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
public Money Multiply(decimal factor)
{
return new Money(Amount * factor, Currency);
}
public static Money Zero(string currency) => new(0, currency);
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public override string ToString()
{
return $"{Amount:F2} {Currency}";
}
}
public class Email : ValueObject
{
public string Value { get; }
public Email(string value)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("Email cannot be empty");
if (!IsValidEmail(value))
throw new ArgumentException("Invalid email format");
Value = value.ToLowerInvariant();
}
private static bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public static implicit operator string(Email email) => email.Value;
}
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
return false;
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
public static bool operator ==(ValueObject left, ValueObject right)
{
return EqualOperator(left, right);
}
public static bool operator !=(ValueObject left, ValueObject right)
{
return NotEqualOperator(left, right);
}
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (left is null ^ right is null)
return false;
return left is null || left.Equals(right);
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !EqualOperator(left, right);
}
}5. Aggregates (Agregados)
Agregados são clusters de entidades e value objects que são tratados como uma unidade para transações. Cada agregado tem uma raiz (aggregate root) que controla o acesso aos outros objetos.
public class Order : AggregateRoot<OrderId>
{
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public List<OrderLine> Lines { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? ConfirmedAt { get; private set; }
public Order(OrderId id, CustomerId customerId)
{
Id = id;
CustomerId = customerId;
Lines = new List<OrderLine>();
Status = OrderStatus.Draft;
TotalAmount = Money.Zero;
CreatedAt = DateTime.UtcNow;
AddDomainEvent(new OrderCreatedEvent(Id, CustomerId));
}
public void AddItem(ProductId productId, Quantity quantity, Money unitPrice)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot add items to confirmed order");
var existingLine = Lines.FirstOrDefault(l => l.ProductId == productId);
if (existingLine != null)
{
existingLine.UpdateQuantity(existingLine.Quantity.Add(quantity));
}
else
{
var line = new OrderLine(productId, quantity, unitPrice);
Lines.Add(line);
}
RecalculateTotal();
}
public void RemoveItem(ProductId productId)
{
if (Status != OrderStatus.Draft)
throw new InvalidOperationException("Cannot remove items from confirmed order");
var line = Lines.FirstOrDefault(l => l.ProductId == productId);
if (line != null)
{
Lines.Remove(line);
RecalculateTotal();
}
}
public void Confirm()
{
if (Lines.Count == 0)
throw new InvalidOperationException("Cannot confirm empty order");
Status = OrderStatus.Confirmed;
ConfirmedAt = DateTime.UtcNow;
AddDomainEvent(new OrderConfirmedEvent(Id));
}
public void Cancel()
{
if (Status == OrderStatus.Cancelled)
throw new InvalidOperationException("Order is already cancelled");
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledEvent(Id));
}
private void RecalculateTotal()
{
TotalAmount = Lines.Aggregate(Money.Zero, (total, line) => total + line.Total);
}
}
public class OrderLine : Entity<OrderLineId>
{
public OrderLineId Id { get; private set; }
public ProductId ProductId { get; private set; }
public Quantity Quantity { get; private set; }
public Money UnitPrice { get; private set; }
public Money Total => UnitPrice.Multiply(Quantity.Value);
public OrderLine(ProductId productId, Quantity quantity, Money unitPrice)
{
Id = new OrderLineId(Guid.NewGuid());
ProductId = productId;
Quantity = quantity;
UnitPrice = unitPrice;
}
public void UpdateQuantity(Quantity newQuantity)
{
Quantity = newQuantity;
}
}
public abstract class AggregateRoot<TId> : Entity<TId> where TId : ValueObject
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}6. Domain Events (Eventos de Domínio)
Domain events representam algo que aconteceu no domínio. Eles permitem que diferentes partes do sistema reajam a mudanças sem acoplamento direto.
public interface IDomainEvent
{
Guid Id { get; }
DateTime OccurredOn { get; }
}
public abstract class DomainEvent : IDomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
public class OrderCreatedEvent : DomainEvent
{
public OrderId OrderId { get; }
public CustomerId CustomerId { get; }
public OrderCreatedEvent(OrderId orderId, CustomerId customerId)
{
OrderId = orderId;
CustomerId = customerId;
}
}
public class OrderConfirmedEvent : DomainEvent
{
public OrderId OrderId { get; }
public OrderConfirmedEvent(OrderId orderId)
{
OrderId = orderId;
}
}
public class OrderCancelledEvent : DomainEvent
{
public OrderId OrderId { get; }
public OrderCancelledEvent(OrderId orderId)
{
OrderId = orderId;
}
}7. Domain Services (Serviços de Domínio)
Domain services contêm lógica de negócio que não pertence naturalmente a uma entidade ou value object específico.
public interface IOrderPricingService
{
Money CalculateTotal(Order order, Customer customer);
}
public class OrderPricingService : IOrderPricingService
{
public Money CalculateTotal(Order order, Customer customer)
{
var subtotal = order.Lines.Aggregate(Money.Zero("USD"), (total, line) => total + line.Total);
var discount = CalculateDiscount(customer, subtotal);
var tax = CalculateTax(subtotal.Subtract(discount));
return subtotal.Subtract(discount).Add(tax);
}
private Money CalculateDiscount(Customer customer, Money subtotal)
{
if (customer.IsPremium)
return subtotal.Multiply(0.1m); // 10% discount for premium customers
return Money.Zero("USD");
}
private Money CalculateTax(Money amount)
{
return amount.Multiply(0.08m); // 8% tax
}
}
public interface IInventoryService
{
Task<bool> IsProductAvailable(ProductId productId, Quantity quantity);
Task ReserveProduct(ProductId productId, Quantity quantity);
}
public class InventoryService : IInventoryService
{
private readonly IProductRepository _productRepository;
public InventoryService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<bool> IsProductAvailable(ProductId productId, Quantity quantity)
{
var product = await _productRepository.GetByIdAsync(productId);
return product?.AvailableQuantity >= quantity;
}
public async Task ReserveProduct(ProductId productId, Quantity quantity)
{
var product = await _productRepository.GetByIdAsync(productId);
if (product == null)
throw new ProductNotFoundException(productId);
if (product.AvailableQuantity < quantity)
throw new InsufficientStockException(productId, quantity);
product.ReserveStock(quantity);
await _productRepository.UpdateAsync(product);
}
}8. Repositories (Repositórios)
Repositories abstraem a persistência de dados e fornecem uma interface orientada ao domínio para acessar agregados.
public interface IRepository<T, TId> where T : AggregateRoot<TId> where TId : ValueObject
{
Task<T> GetByIdAsync(TId id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(TId id);
}
public interface IOrderRepository : IRepository<Order, OrderId>
{
Task<IEnumerable<Order>> GetByCustomerAsync(CustomerId customerId);
Task<IEnumerable<Order>> GetByStatusAsync(OrderStatus status);
Task<IEnumerable<Order>> GetByDateRangeAsync(DateTime startDate, DateTime endDate);
}
public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public OrderRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Order> GetByIdAsync(OrderId id)
{
var orderEntity = await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == id.Value);
if (orderEntity == null)
return null;
return MapToDomain(orderEntity);
}
public async Task<IEnumerable<Order>> GetByCustomerAsync(CustomerId customerId)
{
var orderEntities = await _context.Orders
.Include(o => o.Lines)
.Where(o => o.CustomerId == customerId.Value)
.ToListAsync();
return orderEntities.Select(MapToDomain);
}
public async Task AddAsync(Order order)
{
var orderEntity = MapToEntity(order);
_context.Orders.Add(orderEntity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Order order)
{
var orderEntity = await _context.Orders
.Include(o => o.Lines)
.FirstOrDefaultAsync(o => o.Id == order.Id.Value);
if (orderEntity == null)
throw new OrderNotFoundException(order.Id);
UpdateEntityFromDomain(orderEntity, order);
await _context.SaveChangesAsync();
}
private Order MapToDomain(OrderEntity entity)
{
var order = new Order(new OrderId(entity.Id), new CustomerId(entity.CustomerId));
foreach (var lineEntity in entity.Lines)
{
order.AddItem(
new ProductId(lineEntity.ProductId),
new Quantity(lineEntity.Quantity),
new Money(lineEntity.UnitPrice, "USD")
);
}
if (entity.Status == OrderStatus.Confirmed)
order.Confirm();
return order;
}
private OrderEntity MapToEntity(Order order)
{
return new OrderEntity
{
Id = order.Id.Value,
CustomerId = order.CustomerId.Value,
Status = order.Status,
TotalAmount = order.TotalAmount.Amount,
CreatedAt = order.CreatedAt,
ConfirmedAt = order.ConfirmedAt,
Lines = order.Lines.Select(l => new OrderLineEntity
{
Id = l.Id.Value,
ProductId = l.ProductId.Value,
Quantity = l.Quantity.Value,
UnitPrice = l.UnitPrice.Amount
}).ToList()
};
}
}9. Application Services (Serviços de Aplicação)
Application services orquestram casos de uso da aplicação, coordenando entidades de domínio e serviços.
public interface IOrderApplicationService
{
Task<OrderId> CreateOrderAsync(CreateOrderCommand command);
Task ConfirmOrderAsync(ConfirmOrderCommand command);
Task CancelOrderAsync(CancelOrderCommand command);
Task<OrderDto> GetOrderAsync(GetOrderQuery query);
}
public class OrderApplicationService : IOrderApplicationService
{
private readonly IOrderRepository _orderRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IProductRepository _productRepository;
private readonly IInventoryService _inventoryService;
private readonly IOrderPricingService _pricingService;
private readonly IUnitOfWork _unitOfWork;
private readonly IDomainEventDispatcher _eventDispatcher;
public OrderApplicationService(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IProductRepository productRepository,
IInventoryService inventoryService,
IOrderPricingService pricingService,
IUnitOfWork unitOfWork,
IDomainEventDispatcher eventDispatcher)
{
_orderRepository = orderRepository;
_customerRepository = customerRepository;
_productRepository = productRepository;
_inventoryService = inventoryService;
_pricingService = pricingService;
_unitOfWork = unitOfWork;
_eventDispatcher = eventDispatcher;
}
public async Task<OrderId> CreateOrderAsync(CreateOrderCommand command)
{
var customer = await _customerRepository.GetByIdAsync(command.CustomerId);
if (customer == null)
throw new CustomerNotFoundException(command.CustomerId);
var order = new Order(new OrderId(Guid.NewGuid()), command.CustomerId);
foreach (var item in command.Items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId);
if (product == null)
throw new ProductNotFoundException(item.ProductId);
if (!await _inventoryService.IsProductAvailable(item.ProductId, item.Quantity))
throw new InsufficientStockException(item.ProductId, item.Quantity);
order.AddItem(item.ProductId, item.Quantity, product.Price);
}
await _orderRepository.AddAsync(order);
await _unitOfWork.SaveChangesAsync();
await DispatchDomainEvents(order);
return order.Id;
}
public async Task ConfirmOrderAsync(ConfirmOrderCommand command)
{
var order = await _orderRepository.GetByIdAsync(command.OrderId);
if (order == null)
throw new OrderNotFoundException(command.OrderId);
foreach (var line in order.Lines)
{
await _inventoryService.ReserveProduct(line.ProductId, line.Quantity);
}
order.Confirm();
await _orderRepository.UpdateAsync(order);
await _unitOfWork.SaveChangesAsync();
await DispatchDomainEvents(order);
}
public async Task CancelOrderAsync(CancelOrderCommand command)
{
var order = await _orderRepository.GetByIdAsync(command.OrderId);
if (order == null)
throw new OrderNotFoundException(command.OrderId);
order.Cancel();
await _orderRepository.UpdateAsync(order);
await _unitOfWork.SaveChangesAsync();
await DispatchDomainEvents(order);
}
public async Task<OrderDto> GetOrderAsync(GetOrderQuery query)
{
var order = await _orderRepository.GetByIdAsync(query.OrderId);
if (order == null)
throw new OrderNotFoundException(query.OrderId);
return MapToDto(order);
}
private async Task DispatchDomainEvents(Order order)
{
foreach (var domainEvent in order.DomainEvents)
{
await _eventDispatcher.DispatchAsync(domainEvent);
}
order.ClearDomainEvents();
}
private OrderDto MapToDto(Order order)
{
return new OrderDto
{
Id = order.Id.Value,
CustomerId = order.CustomerId.Value,
Status = order.Status.ToString(),
TotalAmount = order.TotalAmount.Amount,
CreatedAt = order.CreatedAt,
ConfirmedAt = order.ConfirmedAt,
Lines = order.Lines.Select(l => new OrderLineDto
{
ProductId = l.ProductId.Value,
Quantity = l.Quantity.Value,
UnitPrice = l.UnitPrice.Amount,
Total = l.Total.Amount
}).ToList()
};
}
}🚀 Guia para Iniciar uma Aplicação com DDD
Passo 1: Entender o Domínio
- Event Storming: Realize workshops com especialistas de domínio para mapear eventos de negócio
- Domain Experts: Identifique e trabalhe com especialistas de domínio
- Ubiquitous Language: Defina a linguagem compartilhada entre técnicos e especialistas
Passo 2: Identificar Bounded Contexts
// Exemplo de identificação de contextos
public static class BoundedContexts
{
public const string OrderManagement = "OrderManagement";
public const string CustomerManagement = "CustomerManagement";
public const string ProductCatalog = "ProductCatalog";
public const string Inventory = "Inventory";
public const string Payment = "Payment";
public const string Shipping = "Shipping";
}Passo 3: Definir Entidades e Value Objects
// Value Objects
public class OrderId : ValueObject
{
public Guid Value { get; }
public OrderId(Guid value)
{
Value = value;
}
public static OrderId New() => new(Guid.NewGuid());
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}
public class CustomerId : ValueObject
{
public Guid Value { get; }
public CustomerId(Guid value)
{
Value = value;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}
// Enums como Value Objects
public class OrderStatus : ValueObject
{
public static readonly OrderStatus Draft = new("Draft");
public static readonly OrderStatus Confirmed = new("Confirmed");
public static readonly OrderStatus Cancelled = new("Cancelled");
public static readonly OrderStatus Shipped = new("Shipped");
public static readonly OrderStatus Delivered = new("Delivered");
public string Value { get; }
private OrderStatus(string value)
{
Value = value;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}Passo 4: Implementar Agregados
public class Customer : AggregateRoot<CustomerId>
{
public CustomerId Id { get; private set; }
public string Name { get; private set; }
public Email Email { get; private set; }
public CustomerStatus Status { get; private set; }
public DateTime CreatedAt { get; private set; }
public Customer(CustomerId id, string name, Email email)
{
Id = id;
Name = name;
Email = email;
Status = CustomerStatus.Active;
CreatedAt = DateTime.UtcNow;
AddDomainEvent(new CustomerCreatedEvent(Id, Name, Email));
}
public void UpdateProfile(string name, Email email)
{
Name = name;
Email = email;
AddDomainEvent(new CustomerProfileUpdatedEvent(Id, Name, Email));
}
public void Deactivate()
{
Status = CustomerStatus.Inactive;
AddDomainEvent(new CustomerDeactivatedEvent(Id));
}
}Passo 5: Configurar a Estrutura de Pastas
src/
├── Domain/
│ ├── Entities/
│ ├── ValueObjects/
│ ├── Aggregates/
│ ├── DomainEvents/
│ ├── DomainServices/
│ └── Exceptions/
├── Application/
│ ├── Commands/
│ ├── Queries/
│ ├── DTOs/
│ ├── Services/
│ └── Interfaces/
├── Infrastructure/
│ ├── Persistence/
│ ├── ExternalServices/
│ └── Messaging/
└── Presentation/
├── Controllers/
├── Middleware/
└── Filters/Passo 6: Implementar Repositories
public interface IUnitOfWork : IDisposable
{
IOrderRepository Orders { get; }
ICustomerRepository Customers { get; }
IProductRepository Products { get; }
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitAsync();
Task RollbackAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
private IDbContextTransaction _transaction;
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
Orders = new OrderRepository(context);
Customers = new CustomerRepository(context);
Products = new ProductRepository(context);
}
public IOrderRepository Orders { get; }
public ICustomerRepository Customers { get; }
public IProductRepository Products { get; }
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitAsync()
{
await _transaction?.CommitAsync();
}
public async Task RollbackAsync()
{
await _transaction?.RollbackAsync();
}
public void Dispose()
{
_transaction?.Dispose();
_context?.Dispose();
}
}Passo 7: Configurar Dependency Injection
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
services.AddScoped<IOrderPricingService, OrderPricingService>();
services.AddScoped<IInventoryService, InventoryService>();
services.AddScoped<IDomainEventDispatcher, DomainEventDispatcher>();
return services;
}
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddScoped<IOrderApplicationService, OrderApplicationService>();
services.AddScoped<ICustomerApplicationService, CustomerApplicationService>();
return services;
}
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<ICustomerRepository, CustomerRepository>();
services.AddScoped<IProductRepository, ProductRepository>();
return services;
}
}
// Program.cs ou Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDomainServices();
services.AddApplicationServices();
services.AddInfrastructureServices(Configuration);
}Passo 8: Implementar Controllers
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderApplicationService _orderService;
public OrdersController(IOrderApplicationService orderService)
{
_orderService = orderService;
}
[HttpPost]
public async Task<ActionResult<OrderId>> CreateOrder([FromBody] CreateOrderCommand command)
{
try
{
var orderId = await _orderService.CreateOrderAsync(command);
return CreatedAtAction(nameof(GetOrder), new { id = orderId.Value }, orderId);
}
catch (CustomerNotFoundException ex)
{
return NotFound(new { error = ex.Message });
}
catch (ProductNotFoundException ex)
{
return BadRequest(new { error = ex.Message });
}
catch (InsufficientStockException ex)
{
return BadRequest(new { error = ex.Message });
}
}
[HttpGet("{id}")]
public async Task<ActionResult<OrderDto>> GetOrder(Guid id)
{
try
{
var order = await _orderService.GetOrderAsync(new GetOrderQuery { OrderId = new OrderId(id) });
return Ok(order);
}
catch (OrderNotFoundException ex)
{
return NotFound(new { error = ex.Message });
}
}
[HttpPost("{id}/confirm")]
public async Task<IActionResult> ConfirmOrder(Guid id)
{
try
{
await _orderService.ConfirmOrderAsync(new ConfirmOrderCommand { OrderId = new OrderId(id) });
return NoContent();
}
catch (OrderNotFoundException ex)
{
return NotFound(new { error = ex.Message });
}
}
[HttpPost("{id}/cancel")]
public async Task<IActionResult> CancelOrder(Guid id)
{
try
{
await _orderService.CancelOrderAsync(new CancelOrderCommand { OrderId = new OrderId(id) });
return NoContent();
}
catch (OrderNotFoundException ex)
{
return NotFound(new { error = ex.Message });
}
}
}📚 Recursos Adicionais
Livros Recomendados
- "Domain-Driven Design" - Eric Evans
- "Implementing Domain-Driven Design" - Vaughn Vernon
- "Domain-Driven Design Distilled" - Vaughn Vernon
- "Patterns, Principles, and Practices of Domain-Driven Design" - Scott Millett
Artigos e Blogs
- Martin Fowler's Blog
- DDD Community
- Microsoft .NET Documentation
⚠️ Considerações Importantes
Quando Usar DDD
- Domínios Complexos: Quando o domínio de negócio é complexo
- Equipes Grandes: Quando há múltiplas equipes trabalhando no mesmo sistema
- Longa Duração: Para projetos que vão durar anos
- Mudanças Frequentes: Quando os requisitos mudam frequentemente
Quando NÃO Usar DDD
- CRUD Simples: Para aplicações CRUD básicas
- Domínios Simples: Quando o domínio é muito simples
- Projetos Pequenos: Para projetos de curta duração
- Equipes Pequenas: Quando há poucos desenvolvedores
Desafios Comuns
- Curva de Aprendizado: DDD tem uma curva de aprendizado íngreme
- Over-Engineering: Fácil de aplicar demais e complicar desnecessariamente
- Performance: Pode impactar performance se não bem implementado
- Complexidade: Pode adicionar complexidade desnecessária
Boas Práticas
- Comece Pequeno: Implemente DDD gradualmente
- Foque no Domínio: Mantenha o foco no domínio de negócio
- Linguagem Ubíqua: Use consistentemente a linguagem ubíqua
- Teste Adequadamente: Teste entidades, value objects e serviços de domínio
- Documente Decisões: Documente decisões arquiteturais importantes
🎯 Conclusão
Domain-Driven Design é uma abordagem poderosa para desenvolver software complexo que reflete fielmente o domínio de negócio. Quando aplicado corretamente, ele resulta em código mais legível, manutenível e alinhado com as necessidades do negócio.
A chave para o sucesso com DDD é:
- Entender profundamente o domínio
- Usar consistentemente a linguagem ubíqua
- Manter o foco na complexidade do domínio, não na complexidade técnica
- Implementar gradualmente e ajustar conforme necessário
Lembre-se: DDD não é uma solução para todos os problemas, mas uma ferramenta poderosa para domínios complexos que merece ser considerada no arsenal de qualquer desenvolvedor senior.