Knowledge

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.

11/03/20268 min de leituraKnowledge
Domain-Driven Design em 2026: padrões táticos para arquitetura de microsserviços

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

Leituras relacionadas