Domain-Driven Design em 2026: padrões táticos para arquitetura de microsserviços
Como DDD continua relevante em 2026 aplicando bounded contexts, agregados e eventos de domínio para arquiteturas cloud-native.
Resumo executivo
Como DDD continua relevante em 2026 aplicando bounded contexts, agregados e eventos de domínio para arquiteturas cloud-native.
Ultima atualizacao: 11/03/2026
Por que DDD continua relevante em 2026
Domain-Driven Design foi introduzido por Eric Evans em 2003, numa era de monolitos, ORMs e bancos de dados relacionais. Duas décadas depois, vivemos em um mundo de microsserviços, polyglot persistence e arquiteturas event-driven. A pergunta natural: DDD ainda importa?
A resposta curta: mais do que nunca.
Na era dos microsserviços, o maior desafio não é mais persistência ou deployment—é delimitar responsabilidades e manter consistência em sistemas distribuídos. DDD fornece as ferramentas conceituais para transformar complexidade de domínio em uma arquitetura que escala.
Estratégico vs. Tático: o que focar em 2026
DDD tem dois níveis: estratégico (bounded contexts, context mapping) e tático (agregados, value objects, repositórios). Em 2026, a maioria das equipes já entende bounded contexts—cada microsserviço é um bounded context.
O verdadeiro desafio é aplicar padrões táticos dentro de cada serviço. Quando um bounded context cresce de 2 desenvolvedores para 15, como você mantém o código focado no domínio e não se torna um anemic domain model ou procedural spaghetti?
Bounded Contexts como fronteira de arquitetura
Um bounded context é um limite dentro do qual um modelo de domínio particular se aplica. Em arquiteturas de microsserviços, esse limite geralmente mapeia para um serviço.
typescript// Bounded Context: Orders Service
// Contexto: Orquestração de pedidos, status e fulfillment
// Fora de escopo: Pagamentos, Inventário, Catálogo de produtos
type OrderStatus = 'pending' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
interface Order {
id: string
customerId: string
items: OrderItem[]
status: OrderStatus
totalAmount: Money
shippingAddress: Address
createdAt: DateTime
updatedAt: DateTime
}
interface OrderItem {
productId: string
productName: string // Denormalizado: não referencia Products Context
quantity: number
unitPrice: Money
totalPrice: Money
}Regra de ouro: Dentro de um bounded context, termos têm significado consistente. Entre bounded contexts, termos precisam ser traduzidos através de anti-corruption layers.
Context Mapping para sistemas distribuídos
typescript// Anti-Corruption Layer: Orders Context → Payments Context
// No contexto Orders, temos notion de "payment"
interface OrderPayment {
orderId: string
amount: Money
method: PaymentMethod
status: 'pending' | 'completed' | 'failed'
}
// No contexto Payments, a notion é "transaction"
interface PaymentTransaction {
transactionId: string
reference: string // Maps to orderId
amount: Money
gateway: PaymentGateway
status: 'authorized' | 'captured' | 'failed'
gatewayResponse: GatewayResponse
}
// ACL traduz entre modelos
class PaymentsAdapter {
async authorizePayment(orderPayment: OrderPayment): Promise<PaymentTransaction> {
const transaction = await this.paymentsGateway.authorize({
amount: orderPayment.amount,
method: orderPayment.method,
reference: orderPayment.orderId
});
// Traduzir modelo de volta para contexto Orders
return {
transactionId: transaction.id,
orderId: orderPayment.orderId,
amount: orderPayment.amount,
status: this.mapGatewayStatus(transaction.status),
gateway: transaction.gateway
};
}
private mapGatewayStatus(status: string): OrderPayment['status'] {
return status === 'success' ? 'completed' : 'failed';
}
}Agregados: consistência e transações em escala
Agregados são clusters de objetos de domínio que devem ser tratados como uma unidade. Em microsserviços, agregados definem limites de transação e de consistência.
Aggregates em 2026 com TypeScript
typescript// Aggregate Root: Order
class Order {
private items: OrderItem[] = []
private _status: OrderStatus = 'pending'
private _version: number = 0
constructor(
readonly id: string,
readonly customerId: string,
readonly createdAt: DateTime
) {}
// Acesso controlado a membros
get status(): OrderStatus {
return this._status
}
get version(): number {
return this._version
}
// Invariantes protegidos por métodos
addItem(productId: string, name: string, quantity: number, unitPrice: Money): void {
if (this._status !== 'pending' && this._status !== 'confirmed') {
throw new DomainError('Cannot add items to order after processing');
}
const existingItem = this.items.find(item => item.productId === productId);
if (existingItem) {
existingItem.updateQuantity(quantity);
} else {
this.items.push(OrderItem.create(productId, name, quantity, unitPrice));
}
this._version++;
}
confirm(): void {
if (this.items.length === 0) {
throw new DomainError('Cannot confirm empty order');
}
if (this._status !== 'pending') {
throw new DomainError('Order must be pending to confirm');
}
this._status = 'confirmed';
this._version++;
}
// Cálculo derivado sempre consistente
get totalAmount(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.totalPrice),
Money.zero()
);
}
}
// Entity: OrderItem
class OrderItem {
private constructor(
readonly productId: string,
readonly productName: string,
private _quantity: number,
readonly unitPrice: Money
) {}
static create(productId: string, productName: string, quantity: number, unitPrice: Money): OrderItem {
return new OrderItem(productId, productName, quantity, unitPrice);
}
updateQuantity(newQuantity: number): void {
if (newQuantity <= 0) {
throw new DomainError('Quantity must be positive');
}
this._quantity = newQuantity;
}
get quantity(): number {
return this._quantity;
}
get totalPrice(): Money {
return this.unitPrice.multiply(this._quantity);
}
}Invariantes de agregado como defesa de domínio
typescript// Invariantes explicitamente testados
class Order {
// ...existing code...
canBeModified(): boolean {
const modifiableStatuses: OrderStatus[] = ['pending', 'confirmed'];
return modifiableStatuses.includes(this._status);
}
hasMinimumItems(): boolean {
return this.items.length >= 1;
}
itemsTotalWithinLimit(maxItems: number): boolean {
return this.items.length <= maxItems;
}
static maxItemsPerOrder = 50;
}Value Objects: imutabilidade e significado
Value Objects representam conceitos de domínio identificados por seus atributos, não por identidade. Eles eliminam primitive obsession e tornam o código mais expressivo.
typescript// Value Object: Money
class Money {
private constructor(
readonly amount: number,
readonly currency: string
) {
if (amount < 0) {
throw new DomainError('Money amount cannot be negative');
}
if (amount > Number.MAX_SAFE_INTEGER) {
throw new DomainError('Money amount exceeds safe integer limit');
}
}
static zero(currency: string = 'USD'): Money {
return new Money(0, currency);
}
static of(amount: number, currency: string = 'USD'): Money {
return new Money(amount, currency);
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new DomainError('Cannot add money with different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
// Value objects são imutáveis—criamos novos, não modificamos
withDiscount(percentage: number): Money {
const discount = this.amount * (percentage / 100);
return new Money(this.amount - discount, this.currency);
}
}
// Uso em domínio
const orderTotal = Money.of(100, 'USD');
const discounted = orderTotal.withDiscount(10); // Money { amount: 90, currency: 'USD' }
const withTax = discounted.add(Money.of(9, 'USD')); // Money { amount: 99, currency: 'USD' }Domain Events: consistência eventual
Em sistemas distribuídos, a consistência imediata é cara e muitas vezes desnecessária. Domain Events permitem que bounded contexts reajam a mudanças de forma assíncrona.
typescript// Domain Event: Base
interface DomainEvent {
readonly eventType: string
readonly occurredAt: DateTime
readonly aggregateId: string
}
// Eventos de domínio
interface OrderCreatedEvent extends DomainEvent {
eventType: 'OrderCreated'
orderId: string
customerId: string
items: Array<{ productId: string; quantity: number }>
totalAmount: Money
}
interface OrderConfirmedEvent extends DomainEvent {
eventType: 'OrderConfirmed'
orderId: string
totalAmount: Money
}
interface OrderCancelledEvent extends DomainEvent {
eventType: 'OrderCancelled'
orderId: string
reason: string
}
// Aggregate com eventos
class Order {
private _events: DomainEvent[] = []
confirm(): void {
// ...existing validation logic...
this._status = 'confirmed';
this._version++;
// Emitir evento de domínio
this._events.push({
eventType: 'OrderConfirmed',
occurredAt: DateTime.now(),
aggregateId: this.id,
orderId: this.id,
totalAmount: this.totalAmount
} as OrderConfirmedEvent);
}
pullEvents(): DomainEvent[] {
const events = [...this._events];
this._events = [];
return events;
}
hasUncommittedEvents(): boolean {
return this._events.length > 0;
}
}
// Publisher de eventos
class DomainEventPublisher {
async publish(events: DomainEvent[]): Promise<void> {
await Promise.all(events.map(event => this.messageBus.publish(
`orders.${event.eventType}`,
event
)));
}
}Repositórios: abstraindo persistência
Repositórios isolam o modelo de domínio de detalhes de persistência, permitindo que você troque tecnologias sem afetar o domínio.
typescript// Interface de repositório
interface OrderRepository {
save(order: Order): Promise<void>
findById(id: string): Promise<Order | null>
findByCustomer(customerId: string, options?: PaginationOptions): Promise<Order[]>
delete(id: string): Promise<void>
}
// Implementação com Prisma (TypeORM, MikroORM similar)
class PrismaOrderRepository implements OrderRepository {
constructor(private prisma: PrismaClient) {}
async save(order: Order): Promise<void> {
const orderData = this.toPersistence(order);
await this.prisma.$transaction(async (tx) => {
// Upsert order
await tx.order.upsert({
where: { id: order.id },
create: orderData,
update: orderData
});
// Upsert items
for (const item of order.items) {
await tx.orderItem.upsert({
where: { id: `${order.id}:${item.productId}` },
create: this.itemToPersistence(order.id, item),
update: this.itemToPersistence(order.id, item)
});
}
// Persistir eventos de domínio
if (order.hasUncommittedEvents()) {
await this.storeEvents(order.pullEvents());
}
});
}
async findById(id: string): Promise<Order | null> {
const data = await this.prisma.order.findUnique({
where: { id },
include: { items: true }
});
if (!data) return null;
return this.fromPersistence(data);
}
private toPersistence(order: Order): unknown {
return {
id: order.id,
customerId: order.customerId,
status: order.status,
totalAmount: order.totalAmount.amount,
currency: order.totalAmount.currency,
shippingAddress: JSON.stringify(order.shippingAddress),
version: order.version
};
}
private fromPersistence(data: any): Order {
// Reconstituir aggregate do estado persistido
const order = new Order(data.id, data.customerId, data.createdAt);
// Restaurar estado... (simplificado)
return order;
}
}CQRS: separando comandos de queries
Command Query Responsibility Segregation (CQRS) reconhece que modelos otimizados para escrita são diferentes de modelos otimizados para leitura.
typescript// Command Side: Modelo de domínio (como visto acima)
// Query Side: Projeções otimizadas
interface OrderSummary {
id: string
customerId: string
status: OrderStatus
totalAmount: Money
createdAt: DateTime
customerName: string // Denormalizado para queries
}
class OrderQueryService {
constructor(private db: Database) {}
async getCustomerOrders(customerId: string): Promise<OrderSummary[]> {
return this.db.query(`
SELECT
o.id,
o.customerId,
o.status,
o.totalAmount,
o.createdAt,
c.name as customerName
FROM orders o
JOIN customers c ON o.customerId = c.id
WHERE o.customerId = ?
ORDER BY o.createdAt DESC
`, [customerId]);
}
async getOrderDetails(orderId: string): Promise<OrderDetail> {
// Projeção enriquecida com dados de múltiplos bounded contexts
const order = await this.db.queryOne(`
SELECT * FROM orders WHERE id = ?
`, [orderId]);
const items = await this.db.query(`
SELECT * FROM order_items WHERE orderId = ?
`, [orderId]);
return {
...order,
items: items.map(item => ({
...item,
// Enrichment: busca nome atual do produto
productName: this.getCachedProductName(item.productId)
}))
};
}
// Handler de eventos para manter projeções atualizadas
async onOrderConfirmed(event: OrderConfirmedEvent): Promise<void> {
await this.db.query(`
UPDATE orders
SET status = 'confirmed'
WHERE id = ?
`, [event.orderId]);
}
}DDD em 2026: práticas modernas
Event Sourcing
Em vez de persistir apenas o estado atual, persista todos os eventos que levaram a esse estado.
typescriptclass EventSourcedOrder {
private events: DomainEvent[] = []
static async fromHistory(orderId: string, eventStore: EventStore): Promise<EventSourcedOrder> {
const events = await eventStore.getEvents('Order', orderId);
const order = new EventSourcedOrder();
for (const event of events) {
order.apply(event);
}
return order;
}
apply(event: DomainEvent): void {
switch (event.eventType) {
case 'OrderCreated':
// Aplicar lógica de criação
break;
case 'OrderConfirmed':
this._status = 'confirmed';
break;
// ... outros casos
}
this.events.push(event);
}
}Testabilidade com Test Builders
typescript// Test Builder para Order
class OrderTestBuilder {
private orderId = 'test-order-1'
private customerId = 'test-customer-1'
private createdAt = DateTime.fromISO('2026-01-01')
withOrderId(id: string): OrderTestBuilder {
this.orderId = id;
return this;
}
withCustomerId(id: string): OrderTestBuilder {
this.customerId = id;
return this;
}
build(): Order {
const order = new Order(this.orderId, this.customerId, this.createdAt);
return order;
}
}
// Uso em testes
describe('Order', () => {
it('should confirm when has items', () => {
const order = new OrderTestBuilder()
.withOrderId('order-123')
.build();
order.addItem('prod-1', 'Product 1', 1, Money.of(10));
order.confirm();
expect(order.status).toBe('confirmed');
});
});Quando DDD tático é excessivo
DDD tático requer investimento mental significativo. Não é necessário para CRUD simples ou aplicações sem complexidade de domínio.
DDD é justificado quando:
- Domínio tem lógica de negócio não-trivial (regras, invariantes)
- Múltiplos stakeholders terminam a mesma palavra de forma diferente
- Time precisa manter e evoluir código por anos
- Consistência em sistemas distribuídos é um desafio
DDD provavelmente é excessivo quando:
- Aplicação é thin wrapper sobre banco de dados
- Lógica de domínio é trivial (CRUD com validação básica)
- Time de 1-2 pessoas, aplicação de curto prazo
- Performance de hot paths é mais importante que expressividade de domínio
Conclusão
Domain-Driven Design não é uma tecnologia—é um conjunto de ferramentas conceituais para modelar domínios complexos. Em 2026, com arquiteturas de microsserviços e sistemas distribuídos, essas ferramentas são mais relevantes do que nunca.
Bounded contexts fornecem limites claros de responsabilidade. Aggregates definem onde a consistência imediata é necessária. Value Objects eliminam ambiguidade e bugs. Domain Events permitem consistência eventual sem bloqueio.
O investimento em DDD paga em código que expressa o domínio em seus próprios termos, é testável e evolui com o negócio. Em vez de lutar com modelos anêmicos ou código procedural abstrato, sua equipe constrói um sistema que especialistas de domínio podem ler e validar.
Comece identificando bounded contexts em sua arquitetura atual. Aplique padrões táticos no contexto mais complexo. Valide que a equipe entende e valoriza a expressividade de domínio. Expanda gradualmente para outros serviços.
Seu sistema de microsserviços está crescendo e a complexidade de domínio se torna incontrolável? Fale com especialistas em arquitetura da Imperialis para projetar uma estratégia DDD que simplifique complexidade e aumente expressividade de domínio.
Fontes
- Domain-Driven Design by Eric Evans — Livro seminal
- Implementing Domain-Driven Design by Vaughn Vernon — Guia prático
- DDD Community — Comunidade e recursos
- Event Sourcing Reference — Pattern reference