Por que o CRUD comum está matando seu projeto e como o DDD pode consertar isso
\n\nTodo desenvolvedor já se deparou pelo menos uma vez com uma situação em que um projeto aparentemente simples se transforma em um "pântano" de serviços, controladores e lógica de negócios espalhada por todo o código. Você começa escrevendo UserService, depois OrderService, e seis meses depois, dentro de OrderService está a lógica de envio de e-mail, cálculo de descontos e verificação de saldo do usuário. Familiar?
O problema é que pensamos cedo demais em bancos de dados, frameworks e APIs REST. Esquecemos o principal — o domínio (domain). O Domain-Driven Design (DDD) propõe uma abordagem radicalmente diferente: primeiro modelamos os processos de negócio na linguagem dos especialistas, e só depois — o código.
\n\nNeste artigo, vamos explorar como aplicar Domain-Driven Design em TypeScript com exemplos reais. Você verá como construir uma arquitetura limpa, onde as regras de negócio vivem separadas da infraestrutura, e o código é fácil de testar e modificar.
\n\nFundamentos do DDD: linguagem, contextos e modelo
\n\nUbiquitous Language (Linguagem Ubíqua)
\nO primeiro princípio do DDD é uma linguagem comum, compreensível tanto para desenvolvedores quanto para especialistas de negócio. Se o gerente diz "O cliente ativou a assinatura", então no código deve haver um método activateSubscription(), e não setStatus(1). TypeScript, com sua tipagem forte, é ideal para isso — os nomes das classes, métodos e variáveis se tornam documentação.
Bounded Context (Contexto Delimitado)
\nUm sistema grande é dividido em contextos independentes: "Pagamentos", "Gerenciamento de Usuários", "Entrega". Em cada contexto, há seu próprio modelo e linguagem. Os módulos do TypeScript e os tipos estritos ajudam a separar rigidamente esses contextos no nível das importações.
\n\nConstruindo o modelo de domínio em TypeScript
\n\nValue Object (Objeto de Valor)
\nÉ uma entidade imutável que é definida por seus atributos, e não por um identificador. Exemplo: Email, Money, Coordinates.
\n\n// value-objects/Email.ts\nexport class Email {\n private readonly value: string;\n\n constructor(email: string) {\n if (!this.isValid(email)) {\n throw new Error('Invalid email address');\n }\n this.value = email;\n }\n\n private isValid(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n }\n\n public getValue(): string {\n return this.value;\n }\n\n public equals(other: Email): boolean {\n return this.value === other.value;\n }\n}\n\n\nObserve: o Email valida a si mesmo. Nenhum serviço precisa saber como é um e-mail válido. Isso é Domain-Driven Design em TypeScript puro — a regra de negócio vive dentro do objeto.
Entity (Entidade)
\nAo contrário do Value Object, uma entidade possui um identificador (ID) e pode mudar ao longo do tempo. Exemplo: User, Order.
\n\n// entities/User.ts\nimport { Email } from '../value-objects/Email';\nimport { UserId } from '../value-objects/UserId';\n\nexport class User {\n private readonly id: UserId;\n private email: Email;\n private isActive: boolean;\n\n constructor(id: UserId, email: Email) {\n this.id = id;\n this.email = email;\n this.isActive = true;\n }\n\n public getId(): UserId {\n return this.id;\n }\n\n public changeEmail(newEmail: Email): void {\n if (!this.isActive) {\n throw new Error('Cannot change email for inactive user');\n }\n this.email = newEmail;\n }\n\n public deactivate(): void {\n this.isActive = false;\n }\n}\n\n\nAggregate (Agregado)
\nUm agregado é um grupo de entidades e objetos de valor que são tratados como uma unidade coesa. O agregado possui uma raiz (Aggregate Root), através da qual o acesso aos objetos internos é feito.
\n\nExemplo: um pedido (Order)