Por que DDD não é um termo da moda, mas uma ferramenta para salvar projetos
Todo desenvolvedor Java já se deparou com esta situação: o código funciona, a lógica de negócio está espalhada pelos serviços e, depois de seis meses, apenas o autor (e com dificuldade) consegue entender o projeto. Familiar? Este é o clássico anemic domain model — quando as entidades são apenas POJOs com getters e setters, e toda a lógica vive em serviços "gordos".
É aqui que entra o Domain-Driven Design (DDD) — uma abordagem que coloca a lógica de negócio no centro do desenvolvimento, e não os detalhes técnicos. E não, não é mais uma metodologia "teórica". O DDD em projetos Java fornece padrões concretos que transformam o caos em uma arquitetura limpa e sustentável. Neste artigo, vamos explorar o DDD na prática: com código, exemplos e sem rodeios.
DDD Estratégico vs Tático: o que escolher para o seu projeto Java?
O DDD é dividido em dois níveis: estratégico e tático. O primeiro ajuda a dividir um sistema grande em Bounded Contexts (contextos) e a estabelecer a comunicação entre eles. O segundo fornece blocos de construção concretos para o código: Entity, Value Object, Aggregate, Repository, Domain Event, etc.
Para o desenvolvedor Java, o DDD tático é por onde começar. Ele oferece ferramentas que podem ser aplicadas hoje mesmo. Mas, sem a compreensão estratégica dos contextos, você corre o risco de criar um modelo "universal" que não atende a ninguém. Portanto, vamos começar pelo básico: analisar os principais padrões usando um domínio simples, mas real — um sistema de gerenciamento de pedidos.
Blocos de construção do DDD em Java: de Entity a Aggregate
1. Entity (Entidade) — cada uma tem sua própria identidade
No DDD, uma Entity é um objeto que possui um identificador único e uma linha de vida contínua. Você pode alterar todos os seus campos, mas ainda será a mesma entidade. Em Java, não é apenas um POJO — é um objeto com comportamento.
Exemplo: um pedido em uma loja online. Mesmo que o status do pedido mude, ainda é o mesmo pedido com o mesmo ID.
public class Order { private final OrderId id; // Value Object, não primitivo private OrderStatus status; private List<OrderLineItem> items; private Money totalAmount;
public Order(OrderId id) { this.id = id; this.status = OrderStatus.PENDING; this.items = new ArrayList<>(); this.totalAmount = Money.ZERO; }
public void addItem(Product product, int quantity) { // Regra de negócio: não é possível adicionar item a um pedido confirmado if (this.status != OrderStatus.PENDING) { throw new IllegalStateException("Cannot add items to a non-pending order"); } OrderLineItem item = new OrderLineItem(product, quantity); items.add(item); recalculateTotal(); }
public void confirm() { if (items.isEmpty()) { throw new IllegalStateException("Cannot confirm an empty order"); } this.status = OrderStatus.CONFIRMED; // Aqui pode ser disparado um evento de domínio // registerEvent(new OrderConfirmedEvent(this.id)); }
private void recalculateTotal() { this.totalAmount = items.stream() .map(OrderLineItem::getSubtotal) .reduce(Money.ZERO, Money::add); }
// Getters — apenas para leitura, sem setters! public OrderId getId() { return id; } public OrderStatus getStatus() { return status; } public List<OrderLineItem> getItems() { return Collections.unmodifiableList(items); } public Money getTotalAmount() { return totalAmount; }}Observe: OrderId não é um Long, mas sim um Value Object. Por quê? Porque um tipo primitivo não carrega semântica. OrderId pode conter um prefixo, data de criação ou pr