Design Orientado ao Domínio É Só Renomear o Que Você Já Tem
Depois de 47 anos construindo sistemas que de alguma forma ainda rodam em produção, já vi toda tendência arquitetural passar como maré. MVC. SOA. Microsserviços. Reativo. E agora o rei absoluto de todos: Domain-Driven Design.
O DDD é especial. Ele pega o seu UserService.java — que funciona perfeitamente desde 2011 — e o substitui por um UserAggregateRootDomainEntityCommandHandlerFacadeImpl.java que faz exatamente a mesma coisa, exige quatro novos módulos Maven, e precisa de uma sessão de onboarding de 90 minutos antes que qualquer júnior possa tocá-lo.
Isso se chama maturidade arquitetural. Tenho recomendado há anos.
O Que o DDD Realmente É
Eric Evans escreveu o “livro azul” em 2003. São 560 páginas. Ninguém terminou de ler. Tenho três cópias. Li o índice em duas delas.
A ideia central: o software deve modelar o domínio do negócio usando uma Linguagem Ubíqua compartilhada entre desenvolvedores e especialistas do domínio.
Na prática: reuniões de seis horas onde ninguém concorda sobre o que é um “Cliente”.
// Antes do DDD: funciona, legível, resolve o problema
public class UserService {
public User getById(long id) {
return repo.findById(id);
}
public void deactivate(long id) {
User user = repo.findById(id);
user.setActive(false);
repo.save(user);
}
}
// Depois do DDD: lógica idêntica, custa 3 sprints pra explicar
public class UserAggregateRootCommandHandlerFacadeImpl
implements AggregateRootCommandHandler<
UserAggregateRoot,
UserIdentityValueObject,
UserDeactivationCommandSpecification> {
@Override
public UserAggregateRoot handle(
UserIdentityValueObject userId,
UserDeactivationCommandSpecification command,
UserBoundedContextExecutionContext ctx) {
UserAggregateRoot aggregate = userAggregateRootRepository
.findByIdentity(userId)
.orElseThrow(() -> new UserAggregateRootNotFoundInBoundedContextDomainException(
"Nenhum UserAggregateRoot encontrado para a identidade " + userId.getValue()
+ " no bounded context: " + ctx.getBoundedContextName()
));
aggregate.applyDeactivationCommand(command);
userAggregateRootRepository.persist(aggregate);
return aggregate;
}
}
A segunda versão tem 28 linhas e faz user.setActive(false). As 26 linhas extras são a arquitetura. É aí que está o valor.
Pode confiar em mim.
A Linguagem Ubíqua: Todo Mundo Usa as Mesmas Palavras de Formas Diferentes
A Linguagem Ubíqua garante que desenvolvedores e pessoas de negócio usem terminologia idêntica. Um dicionário só. Sem ambiguidade. Clareza pura.
Eis como fica sua terminologia seis meses depois de adotar DDD:
| Time de Negócio | Time Dev | DBA | Docs da API | Time Mobile |
|---|---|---|---|---|
| Cliente | UserAggregateRoot | tbl_user_master |
account_holder |
person_entity |
| Pedido | PurchaseCommandEvent | tbl_order_hdr |
transaction_record |
cart_submission |
| Cancelar | DeactivationDomainCommand | is_active = 0 |
archive_entity |
delete_action |
| “O sistema” | ProductionEnvironment | Schema PROD |
backend_service |
“a API” |
Linguagem Ubíqua conquistada. Todo mundo está confuso exatamente da mesma forma.
“Dogbert, o que exatamente é uma Linguagem Ubíqua?” “É a linguagem que todo mundo usa mas ninguém entende. Como documentos jurídicos, mas com mais interfaces.” — Dogbert, consultando por $400/hora, Dilbert
Bounded Contexts: Concordando Formalmente em Discordar
Um Bounded Context é uma área onde seu modelo faz sentido. Fora desse contexto, as mesmas palavras significam outra coisa. Isso é normal e definitivamente escalável.
Antes do DDD:
-- Uma tabela users. Todo mundo sabe o que é.
SELECT * FROM users WHERE id = 42;
Depois do DDD, a linha 42 da tabela users é conhecida como:
UserAggregateRoot— Bounded Context de Gerenciamento de UsuáriosSecurityPrincipal— Bounded Context de AutenticaçãoAccountHolder— Bounded Context de FaturamentoRecipientIdentity— Bounded Context de NotificaçõesUserView— Bounded Context de Relatóriostbl_user_master— Bounded Context Legado (ninguém mexe)UserEntity— Bounded Context da Reescrita (alguém começou em janeiro)
Todos os sete ficam sincronizados via eventos que falham silenciosamente no Kafka toda terça-feira.
XKCD 927 já sabia: sempre que há N padrões concorrentes, alguém inventa um novo para unificá-los. DDD é o padrão unificado para modelar sua tabela users. Agora temos N+1.
Aggregates: Constraints de Banco de Dados, Mas Feitos em Java em Runtime
O banco de dados relacional vem garantindo integridade referencial desde antes de eu ter cabelo. Chaves primárias. Chaves estrangeiras. Constraints. Transações. Tudo de graça.
DDD diz: e se a gente reconstruísse isso manualmente? Em lógica de negócio? Durante a sprint 14?
// SQL: garante isso de graça, pra sempre, atomicamente
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INT NOT NULL CHECK (quantity > 0)
);
// DDD Aggregate: agora é responsabilidade sua. Boa sorte.
public class OrderAggregateRoot extends AbstractAggregateRoot<OrderId> {
private final List<OrderLineItemEntity> lineItems = new ArrayList<>();
private OrderStatusValueObject status;
public void addLineItem(ProductIdValueObject productId, QuantityValueObject qty) {
// Validação que o banco faria automaticamente, de graça
if (qty.isNegative()) {
throw new InvalidQuantityDomainException("Sério?");
}
// Regra de negócio antes garantida por uma transação
if (this.status.equals(OrderStatusValueObject.COMPLETED)) {
throw new CannotModifyCompletedOrderDomainException(
"Não é possível adicionar itens a um pedido concluído. " +
"Vide regra de negócio BIZ-2847 no doc de 2019 que ninguém acha."
);
}
this.lineItems.add(new OrderLineItemEntity(productId, qty));
// Registra evento de domínio (dispara se lembrarmos de tratar)
this.registerEvent(
new OrderLineItemAddedDomainEvent(this.id, productId, qty, LocalDateTime.now())
);
}
}
Tudo que o banco tratava automaticamente agora é problema seu, em Java, em horário comercial, em código que vai ter bugs.
Value Objects: Embrulhando Primitivos em Segurança de Emprego
Por que guardar um e-mail como String quando você pode:
public final class EmailAddressValueObject {
private final String value;
private EmailAddressValueObject(String value) {
Objects.requireNonNull(value);
if (!value.contains("@")) {
throw new InvalidEmailAddressValueObjectDomainException(
"'" + value + "' não é um EmailAddressValueObject válido " +
"no BoundedContext de Gerenciamento de Usuários"
);
}
this.value = value.toLowerCase().trim();
}
public static EmailAddressValueObject of(String raw) {
return new EmailAddressValueObject(raw);
}
// getValue(), equals(), hashCode(), toString()...
// ~60 linhas a mais
public String getValue() {
return this.value; // É a String. Sempre foi a String.
}
}
// Antes:
String email = "usuario@example.com"; // 1 linha
// Depois:
EmailAddressValueObject email = EmailAddressValueObject.of("usuario@example.com");
// 1 linha, mais 80 linhas de infraestrutura, mais um módulo novo, mais um mapper
Type safety. Expressividade de domínio. Imutabilidade. LER repetitivo digitando ValueObject no final de cada classe. A experiência DDD completa.
O Imposto da Arquitetura Hexagonal
Você não pode só fazer DDD. Precisa da Arquitetura Hexagonal também. Também chamada de Ports & Adapters. Também chamada de Arquitetura Cebola. Também chamada de Clean Architecture. Também chamada de “Por Que Tem 14 Camadas Pra Um CRUD?”.
Antes do DDD:
src/
├── controllers/ ← HTTP chega aqui
├── services/ ← lógica fica aqui
└── repositories/ ← SQL acontece aqui
Depois do DDD + Arquitetura Hexagonal:
src/
├── domain/
│ ├── model/
│ │ ├── aggregates/
│ │ ├── entities/
│ │ ├── valueobjects/
│ │ └── shared/kernel/
│ ├── events/
│ │ ├── domain/
│ │ └── integration/
│ ├── commands/
│ ├── queries/
│ └── ports/
│ ├── inbound/
│ └── outbound/
├── application/
│ ├── usecases/
│ ├── commandhandlers/
│ ├── queryhandlers/
│ └── eventhandlers/
└── infrastructure/
├── adapters/
│ ├── inbound/
│ │ ├── rest/
│ │ └── messaging/
│ └── outbound/
│ ├── persistence/
│ │ ├── jpa/
│ │ └── mappers/
│ └── messaging/
└── configuration/
O sistema faz exatamente o que fazia antes. Um engenheiro novo agora precisa de três meses pra descobrir onde fica a validação de e-mail.
Será que vale a pena o tempo? Pelos meus cálculos: se o app serve 200 usuários internos, vai ser reescrito em 18 meses de qualquer forma, e tem 3 engenheiros — não. Segundo consultores de DDD: sempre.
Como Vender DDD pro Seu Time
- Diga “alinhar com o domínio do negócio” em toda reunião
- Desenhe caixas conectadas por setas no quadro branco
- Escreva “Bounded Context” em cada caixa
- Chame as setas de “camadas anticorrupção”
- Peça o livro azul (deixe bem visível; não leia)
- Renomeie todas as classes para terminar em
AggregateRoot,ValueObjectouDomainEvent - Crie um canal
#modelagem-de-dominiono Slack - Promova “sessões de alinhamento” semanais de 3 horas
- Após 6 meses: anuncie que “identificaram os bounded contexts”
- Após 12 meses: comece a questionar se escolheram os bounded contexts certos
- Após 18 meses: inicie a reescrita do zero nos bounded contexts corretos
Nada mudou em produção. O diagrama de arquitetura ficou impecável.
“Wally, o que o DDD entregou neste trimestre?” “Renomeamos o UserService para UserAggregateRootDomainCommandHandlerFacade.” “E o que ele faz de diferente?” “Tem um nome maior.” — Wally explica o roadmap do Q3 para o Chefe
Implicações para a Carreira
Adicione isso ao seu LinkedIn imediatamente: Domain-Driven Design, Bounded Contexts, Aggregate Roots, Value Objects, Domain Events, CQRS, Event Sourcing, Arquitetura Hexagonal, Ports & Adapters, Anti-Corruption Layer.
Você agora é um Arquiteto de Domínio. Taxa: 3x a atual. O código não importa. O vocabulário importa.
O Chefe uma vez me perguntou o que tínhamos conquistado depois de um ano de refatoração DDD. Mostrei o diagrama de arquitetura. Ele imprimiu e colocou na apresentação trimestral do conselho. Orçamento aprovado para o Ano 2.
O autor vem modelando o “Domínio de Usuário” desde 1994. O UserAggregateRoot atual tem 847 métodos. O UserService original tinha 12. Ambos executam SELECT * FROM users WHERE id = ?.