<h2>Fundamentos do Domain-Driven Design</h2>
<p>Domain-Driven Design (DDD) é uma metodologia de desenvolvimento que coloca o domínio de negócio no centro da arquitetura de software. Em vez de estruturar o código ao redor de camadas técnicas (controllers, services, repositories), DDD nos ensina a modelar nosso código de forma que ele reflita o domínio real do problema que estamos resolvendo. Isso resulta em código mais legível, maintível e alinhado com as expectativas do negócio.</p>
<p>No contexto de TypeScript, DDD ganha uma vantagem crucial: o sistema de tipos. TypeScript permite que expressemos as regras de negócio através de tipos, garantindo que apenas operações válidas sejam permitidas em tempo de compilação. Isso cria uma barreira de proteção que impede bugs sutis antes mesmo do código rodar. Vamos explorar como Entidades e Value Objects — dois conceitos-chave do DDD — funcionam na prática com TypeScript.</p>
<h2>Entidades: Identidade e Continuidade</h2>
<h3>O Conceito de Entidade</h3>
<p>Uma Entidade no DDD é um objeto que possui identidade única e imutabilidade ao longo do tempo. Diferente de um simples objeto de transferência de dados (DTO), uma Entidade carrega comportamento e regras de negócio. A identidade é o que torna duas entidades diferentes, mesmo que todos os seus atributos sejam iguais. Imagine dois usuários com o mesmo nome e email — eles são pessoas diferentes se possuem IDs diferentes.</p>
<p>A chave é que uma Entidade é identificada por sua identidade, não por seus valores. Se você muda o email de um usuário, ele continua sendo a mesma pessoa (mesma entidade) — apenas seus atributos mudaram.</p>
<h3>Implementando Entidades em TypeScript</h3>
<pre><code class="language-typescript">// Criar um tipo para representar a identidade de forma type-safe
type UserId = string & { readonly __brand: 'UserId' };
function createUserId(value: string): UserId {
if (!value || value.trim().length === 0) {
throw new Error('UserId não pode estar vazio');
}
return value as UserId;
}
// A classe Entidade base fornece comportamento comum
abstract class Entity<T> {
protected readonly _id: T;
constructor(id: T) {
this._id = id;
}
getId(): T {
return this._id;
}
equals(other: Entity<T>): boolean {
return this._id === other._id;
}
}
// Implementar uma entidade de domínio específica
class User extends Entity<UserId> {
private _email: string;
private _name: string;
private _createdAt: Date;
constructor(id: UserId, email: string, name: string, createdAt: Date) {
super(id);
this._email = email;
this._name = name;
this._createdAt = createdAt;
}
static create(email: string, name: string): User {
const id = createUserId(user_${Date.now()}_${Math.random()});
return new User(id, email, name, new Date());
}
getEmail(): string {
return this._email;
}
changeName(newName: string): void {
if (!newName || newName.trim().length === 0) {
throw new Error('Nome não pode estar vazio');
}
this._name = newName;
}
getName(): string {
return this._name;
}
getCreatedAt(): Date {
return this._createdAt;
}
}
// Usando a entidade
const user1 = User.create('joao@example.com', 'João');
const user2 = User.create('joao@example.com', 'João');
console.log(user1.equals(user2)); // false - identidades diferentes
console.log(user1.getId() === user2.getId()); // false
user1.changeName('João Silva');
console.log(user1.getName()); // "João Silva"</code></pre>
<p>Neste exemplo, a classe <code>User</code> é uma Entidade porque possui uma identidade única (<code>UserId</code>) que a distingue de qualquer outra Entidade, mesmo que seus atributos sejam idênticos. O tipo <code>UserId</code> é um branded type — um padrão TypeScript que garante type-safety sem overhead em runtime.</p>
<h2>Value Objects: Imutabilidade e Significado</h2>
<h3>O Conceito de Value Object</h3>
<p>Um Value Object é fundamentalmente diferente de uma Entidade. Enquanto uma Entidade é identificada por sua identidade, um Value Object é identificado por seus valores. Se dois Value Objects possuem os mesmos valores, eles são equivalentes — não importa se são a mesma instância em memória. Um Value Object é sempre imutável: uma vez criado, nunca muda. Se você precisa de um estado diferente, cria um novo Value Object.</p>
<p>Value Objects encapsulam regras de negócio simples mas importantes. Eles garantem que estados inválidos nunca existam no seu domínio. Por exemplo, um Email nunca será criado se não for válido.</p>
<h3>Implementando Value Objects em TypeScript</h3>
<pre><code class="language-typescript">// Value Object para Email com validação
class Email {
private readonly _value: string;
private constructor(value: string) {
this._value = value;
}
static create(value: string): Email {
const trimmed = value.trim().toLowerCase();
if (!this.isValid(trimmed)) {
throw new Error(Email inválido: ${value});
}
return new Email(trimmed);
}
private static isValid(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
getValue(): string {
return this._value;
}
equals(other: Email): boolean {
return this._value === other._value;
}
toString(): string {
return this._value;
}
}
// Value Object para um endereço
class Address {
private readonly _street: string;
private readonly _city: string;
private readonly _zipCode: string;
private readonly _country: string;
private constructor(street: string, city: string, zipCode: string, country: string) {
this._street = street;
this._city = city;
this._zipCode = zipCode;
this._country = country;
}
static create(street: string, city: string, zipCode: string, country: string): Address {
if (!street || !city || !zipCode || !country) {
throw new Error('Todos os campos do endereço são obrigatórios');
}
if (zipCode.length < 5) {
throw new Error('CEP deve ter pelo menos 5 caracteres');
}
return new Address(street, city, zipCode, country);
}
getStreet(): string {
return this._street;
}
getCity(): string {
return this._city;
}
getZipCode(): string {
return this._zipCode;
}
getCountry(): string {
return this._country;
}
equals(other: Address): boolean {
return (
this._street === other._street &&
this._city === other._city &&
this._zipCode === other._zipCode &&
this._country === other._country
);
}
toString(): string {
return ${this._street}, ${this._city} - ${this._zipCode}, ${this._country};
}
}
// Testando os Value Objects
const email1 = Email.create('usuario@example.com');
const email2 = Email.create('USUARIO@EXAMPLE.COM');
console.log(email1.equals(email2)); // true - mesmo valor
const address1 = Address.create('Rua A', 'São Paulo', '01310-100', 'Brasil');
const address2 = Address.create('Rua A', 'São Paulo', '01310-100', 'Brasil');
console.log(address1.equals(address2)); // true - mesmos valores
// Isso lançará erro
try {
Email.create('email-invalido');
} catch (error) {
console.log(error.message); // Email inválido: email-invalido
}</code></pre>
<p>Note que os Value Objects têm construtores privados. Isso força o uso do método <code>create()</code>, onde as validações ocorrem. Essa é uma prática crucial: garante que nenhum Value Object inválido possa existir no seu domínio.</p>
<h2>Integrando Entidades e Value Objects</h2>
<h3>Construindo Agregados Coesos</h3>
<p>Um Agregado é um cluster de Entidades e Value Objects que trabalham juntos para manter as invariantes de negócio. A Entidade raiz (root) do Agregado é responsável pela integridade de todo o grupo. Value Objects são frequentemente usados dentro de Entidades para expressar atributos complexos de forma type-safe.</p>
<p>Vamos criar um exemplo prático onde uma Entidade <code>User</code> utiliza múltiplos Value Objects:</p>
<pre><code class="language-typescript">// Value Object para representar um telefone
class PhoneNumber {
private readonly _value: string;
private constructor(value: string) {
this._value = value;
}
static create(value: string): PhoneNumber {
const cleaned = value.replace(/\D/g, '');
if (cleaned.length < 10 || cleaned.length > 11) {
throw new Error('Telefone deve ter entre 10 e 11 dígitos');
}
return new PhoneNumber(cleaned);
}
getValue(): string {
return this._value;
}
equals(other: PhoneNumber): boolean {
return this._value === other._value;
}
toString(): string {
return this._value;
}
}
// Entidade melhorada que usa Value Objects
class Customer extends Entity<UserId> {
private _email: Email;
private _name: string;
private _address: Address;
private _phone: PhoneNumber;
private _createdAt: Date;
constructor(
id: UserId,
email: Email,
name: string,
address: Address,
phone: PhoneNumber,
createdAt: Date
) {
super(id);
this._email = email;
this._name = name;
this._address = address;
this._phone = phone;
this._createdAt = createdAt;
}
static create(
emailValue: string,
name: string,
street: string,
city: string,
zipCode: string,
country: string,
phoneValue: string
): Customer {
const id = createUserId(customer_${Date.now()}_${Math.random()});
const email = Email.create(emailValue);
const address = Address.create(street, city, zipCode, country);
const phone = PhoneNumber.create(phoneValue);
return new Customer(id, email, name, address, phone, new Date());
}
getEmail(): Email {
return this._email;
}
getName(): string {
return this._name;
}
getAddress(): Address {
return this._address;
}
getPhone(): PhoneNumber {
return this._phone;
}
updateAddress(newAddress: Address): void {
this._address = newAddress;
}
updatePhone(newPhone: PhoneNumber): void {
this._phone = newPhone;
}
getCreatedAt(): Date {
return this._createdAt;
}
}
// Utilizando o agregado
const customer = Customer.create(
'maria@example.com',
'Maria Silva',
'Avenida Principal 123',
'Rio de Janeiro',
'20000-000',
'Brasil',
'21987654321'
);
console.log(customer.getEmail().toString()); // maria@example.com
console.log(customer.getAddress().toString()); // Avenida Principal 123, Rio de Janeiro - 20000-000, Brasil
console.log(customer.getPhone().toString()); // 21987654321
// Updatando o endereço com um novo Value Object
const newAddress = Address.create('Rua Secundária 456', 'Niterói', '24000-000', 'Brasil');
customer.updateAddress(newAddress);
console.log(customer.getAddress().toString()); // Rua Secundária 456, Niterói - 24000-000, Brasil</code></pre>
<h3>Mantendo Invariantes de Domínio</h3>
<p>As invariantes são as regras críticas do seu negócio que nunca devem ser violadas. TypeScript permite expressar essas regras através de tipos e lógica de validação. O exemplo anterior já demonstra isso: um <code>Email</code> inválido nunca pode existir porque a validação é feita no <code>create()</code>.</p>
<p>Vamos criar um exemplo mais complexo com invariantes mais sofisticadas:</p>
<pre><code class="language-typescript">// Value Object para Money (dinheiro)
class Money {
private readonly _amount: number;
private readonly _currency: string;
private constructor(amount: number, currency: string) {
this._amount = amount;
this._currency = currency;
}
static create(amount: number, currency: string = 'BRL'): Money {
if (amount < 0) {
throw new Error('Valor monetário não pode ser negativo');
}
if (!['BRL', 'USD', 'EUR'].includes(currency)) {
throw new Error('Moeda não suportada');
}
return new Money(amount, currency);
}
getAmount(): number {
return this._amount;
}
getCurrency(): string {
return this._currency;
}
add(other: Money): Money {
if (this._currency !== other._currency) {
throw new Error('Não é possível somar moedas diferentes');
}
return Money.create(this._amount + other._amount, this._currency);
}
subtract(other: Money): Money {
if (this._currency !== other._currency) {
throw new Error('Não é possível subtrair moedas diferentes');
}
const result = this._amount - other._amount;
return Money.create(Math.max(result, 0), this._currency);
}
equals(other: Money): boolean {
return this._amount === other._amount && this._currency === other._currency;
}
toString(): string {
return ${this._currency} ${this._amount.toFixed(2)};
}
}
// Value Object para Status de Pedido
type OrderStatus = 'PENDING' | 'CONFIRMED' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';
class OrderStatusVO {
private readonly _status: OrderStatus;
private constructor(status: OrderStatus) {
this._status = status;
}
static create(status: OrderStatus): OrderStatusVO {
const validStatuses: OrderStatus[] = ['PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED', 'CANCELLED'];
if (!validStatuses.includes(status)) {
throw new Error(Status inválido: ${status});
}
return new OrderStatusVO(status);
}
static pending(): OrderStatusVO {
return new OrderStatusVO('PENDING');
}
canTransitionTo(newStatus: OrderStatus): boolean {
const transitions: Record<OrderStatus, OrderStatus[]> = {
'PENDING': ['CONFIRMED', 'CANCELLED'],
'CONFIRMED': ['SHIPPED', 'CANCELLED'],
'SHIPPED': ['DELIVERED'],
'DELIVERED': [],
'CANCELLED': []
};
return transitions[this._status]?.includes(newStatus) ?? false;
}
getStatus(): OrderStatus {
return this._status;
}
equals(other: OrderStatusVO): boolean {
return this._status === other._status;
}
toString(): string {
return this._status;
}
}
// Entidade Order que usa os Value Objects acima
type OrderId = string & { readonly __brand: 'OrderId' };
function createOrderId(value: string): OrderId {
if (!value || value.trim().length === 0) {
throw new Error('OrderId não pode estar vazio');
}
return value as OrderId;
}
class Order extends Entity<OrderId> {
private _customerId: UserId;
private _totalAmount: Money;
private _status: OrderStatusVO;
private _createdAt: Date;
private _items: Array<{ productName: string; quantity: number; price: Money }>;
constructor(
id: OrderId,
customerId: UserId,
totalAmount: Money,
status: OrderStatusVO,
createdAt: Date,
items: Array<{ productName: string; quantity: number; price: Money }>
) {
super(id);
this._customerId = customerId;
this._totalAmount = totalAmount;
this._status = status;
this._createdAt = createdAt;
this._items = items;
}
static create(customerId: UserId): Order {
const id = createOrderId(order_${Date.now()}_${Math.random()});
const totalAmount = Money.create(0);
const status = OrderStatusVO.pending();
return new Order(id, customerId, totalAmount, status, new Date(), []);
}
addItem(productName: string, quantity: number, price: Money): void {
if (this._status.getStatus() !== 'PENDING') {
throw new Error('Não é possível adicionar itens a um pedido que não está pendente');
}
if (quantity <= 0) {
throw new Error('Quantidade deve ser maior que zero');
}
this._items.push({ productName, quantity, price });
this._totalAmount = this._totalAmount.add(price);
}
confirm(): void {
if (!this._status.canTransitionTo('CONFIRMED')) {
throw new Error(Não é possível confirmar um pedido em status ${this._status.toString()});
}
this._status = new OrderStatusVO('CONFIRMED');
}
ship(): void {
if (!this._status.canTransitionTo('SHIPPED')) {
throw new Error(Não é possível enviar um pedido em status ${this._status.toString()});
}
this._status = new OrderStatusVO('SHIPPED');
}
getTotalAmount(): Money {
return this._totalAmount;
}
getStatus(): OrderStatusVO {
return this._status;
}
getItems() {
return [...this._items]; // Retorna cópia para manter imutabilidade
}
}
// Demonstrando o uso
const orderId = createOrderId(order_123);
const order = Order.create(createUserId('user_456'));
const productPrice = Money.create(100, 'BRL');
order.addItem('Notebook', 1, productPrice);
console.log(order.getStatus().toString()); // PENDING
console.log(order.getTotalAmount().toString()); // BRL 100.00
order.confirm();
console.log(order.getStatus().toString()); // CONFIRMED
order.ship();
console.log(order.getStatus().toString()); // SHIPPED
// Isso lançará erro - não pode adicionar itens a um pedido confirmado
try {
order.addItem('Mouse', 2, Money.create(50, 'BRL'));
} catch (error) {
console.log(error.message); // Não é possível adicionar itens a um pedido que não está pendente
}</code></pre>
<h2>Conclusão</h2>
<p>Dominando Domain-Driven Design com TypeScript, você aprenderá que <strong>a segurança de tipos não é apenas uma característica da linguagem, mas um aliado poderoso para expressar regras de negócio</strong>. Usando branded types e construtores privados em Value Objects e Entidades, você cria barreiras que tornam estados inválidos literalmente impossíveis de representar em código — erros são detectados em compile-time, não em produção.</p>
<p>Em segundo lugar, <strong>separar Entidades de Value Objects cria clareza conceptual sobre o seu domínio</strong>. Entidades com identidade para coisas que mudam ao longo do tempo e Value Objects para conceitos imutáveis e auto-contidos faz com que seu código se torne uma documentação viva do negócio. Um novo desenvolvedor entendendo o código entende o domínio.</p>
<p>Por fim, <strong>usar Agregados para encapsular Entidades e Value Objects juntos garante que as invariantes de domínio sejam sempre mantidas</strong>. Nenhum estado inválido pode ser criado acidentalmente porque as regras estão codificadas na própria estrutura do tipo. Isso reduz bugs sutis e torna a manutenção mais previsível.</p>
<h2>Referências</h2>
<ul>
<li>https://www.domainlanguage.com/ddd/ — Domínio oficial de Eric Evans, criador do DDD</li>
<li>https://www.typescriptlang.org/docs/ — Documentação oficial do TypeScript</li>
<li>https://khalilstemmler.com/articles/typescript-domain-driven-design/ — Artigo prático sobre DDD com TypeScript</li>
<li>https://martinfowler.com/bliki/ValueObject.html — Explicação de Martin Fowler sobre Value Objects</li>
<li>https://github.com/stemmlerjs/white-label-apartments — Exemplo de projeto real usando DDD em TypeScript</li>
</ul>
<p><!-- FIM --></p>