Pular para o conteúdo
Software Engineering 15 min read

Arquitetura de Microserviços Event-Driven: Padrões e Práticas

Guia completo sobre arquitetura event-driven para microserviços, incluindo Event Sourcing, CQRS, Saga Pattern, e quando usar cada abordagem.

Por Equipe Integr8 26/12/2024

Por que Event-Driven?

Em arquiteturas de microserviços, a comunicação síncrona (REST/gRPC) cria acoplamento temporal: se um serviço está indisponível, toda a cadeia falha. Arquiteturas event-driven resolvem isso através de comunicação assíncrona baseada em eventos.

🔓

Loose Coupling

Serviços não precisam conhecer uns aos outros diretamente

📈

Escalabilidade

Consumidores podem ser escalados independentemente

🔄

Resiliência

Falhas são isoladas, eventos podem ser reprocessados

⏱️

Temporalidade

Histórico completo de eventos para auditoria e replay

Padrões Fundamentais

1. Event Notification

O padrão mais simples: publicar eventos para notificar outros serviços de mudanças.

// Serviço de Orders publica evento
interface OrderCreatedEvent {
  eventType: 'OrderCreated';
  timestamp: Date;
  data: {
    orderId: string;
    customerId: string;
    items: Array<{ productId: string; quantity: number }>;
    totalAmount: number;
  };
}

// Producer
async function createOrder(order: Order): Promise<void> {
  // Persiste a ordem
  await orderRepository.save(order);

  // Publica evento
  await eventBus.publish('orders', {
    eventType: 'OrderCreated',
    timestamp: new Date(),
    data: {
      orderId: order.id,
      customerId: order.customerId,
      items: order.items,
      totalAmount: order.totalAmount,
    },
  });
}

// Consumer (Serviço de Inventory)
eventBus.subscribe('orders', async (event: OrderCreatedEvent) => {
  if (event.eventType === 'OrderCreated') {
    for (const item of event.data.items) {
      await inventoryService.reserveStock(item.productId, item.quantity);
    }
  }
});

2. Event-Carried State Transfer

Eventos carregam dados suficientes para que consumidores não precisem buscar mais informações.

// ❌ Evento pobre - força consumer a buscar dados
interface OrderCreatedPoor {
  orderId: string;  // Consumer precisa chamar API de Orders
}

// ✅ Evento rico - contém tudo que consumers precisam
interface OrderCreatedRich {
  eventType: 'OrderCreated';
  timestamp: Date;
  data: {
    orderId: string;
    customerId: string;
    customerEmail: string;  // Para serviço de notificação
    customerName: string;
    shippingAddress: Address;  // Para serviço de shipping
    items: Array<{
      productId: string;
      productName: string;
      quantity: number;
      unitPrice: number;
    }>;
    totalAmount: number;
    paymentMethod: string;
  };
}
💡Trade-off

Eventos ricos reduzem chamadas entre serviços, mas aumentam o tamanho das mensagens e podem expor mais dados do que necessário.

3. Event Sourcing

Em vez de armazenar apenas o estado atual, armazena todos os eventos que levaram àquele estado.

// Event Store para Conta Bancária
interface AccountEvent {
  eventId: string;
  accountId: string;
  eventType: string;
  timestamp: Date;
  data: unknown;
}

interface AccountOpened {
  eventType: 'AccountOpened';
  data: { initialBalance: number; ownerName: string };
}

interface MoneyDeposited {
  eventType: 'MoneyDeposited';
  data: { amount: number; description: string };
}

interface MoneyWithdrawn {
  eventType: 'MoneyWithdrawn';
  data: { amount: number; description: string };
}

// Reconstruir estado a partir de eventos
function rebuildAccount(events: AccountEvent[]): Account {
  return events.reduce((account, event) => {
    switch (event.eventType) {
      case 'AccountOpened':
        return {
          id: event.accountId,
          balance: event.data.initialBalance,
          ownerName: event.data.ownerName,
          isOpen: true,
        };
      case 'MoneyDeposited':
        return { ...account, balance: account.balance + event.data.amount };
      case 'MoneyWithdrawn':
        return { ...account, balance: account.balance - event.data.amount };
      case 'AccountClosed':
        return { ...account, isOpen: false };
      default:
        return account;
    }
  }, {} as Account);
}

// Persistir novo evento
async function deposit(accountId: string, amount: number): Promise<void> {
  const events = await eventStore.getEvents(accountId);
  const account = rebuildAccount(events);

  if (!account.isOpen) {
    throw new Error('Account is closed');
  }

  await eventStore.append(accountId, {
    eventType: 'MoneyDeposited',
    data: { amount, description: 'Deposit' },
  });
}

4. CQRS (Command Query Responsibility Segregation)

Separa o modelo de escrita (commands) do modelo de leitura (queries).

Write Side persiste eventos, Read Side consome eventos e projeta para leitura otimizada
100%
Arquitetura CQRS

Write Side persiste eventos, Read Side consome eventos e projeta para leitura otimizada

// Write Side - Domain Model rico
class Order {
  private events: DomainEvent[] = [];

  static create(customerId: string, items: OrderItem[]): Order {
    const order = new Order();
    order.apply(new OrderCreated(customerId, items));
    return order;
  }

  addItem(productId: string, quantity: number): void {
    this.apply(new ItemAdded(this.id, productId, quantity));
  }

  private apply(event: DomainEvent): void {
    this.events.push(event);
    this.when(event);
  }

  private when(event: DomainEvent): void {
    // Atualiza estado interno
  }
}

// Read Side - Projection otimizada para queries
interface OrderReadModel {
  orderId: string;
  customerName: string;
  customerEmail: string;
  status: string;
  totalAmount: number;
  itemCount: number;
  createdAt: Date;
  // Dados denormalizados para evitar joins
}

class OrderProjection {
  async handle(event: DomainEvent): Promise<void> {
    switch (event.type) {
      case 'OrderCreated':
        await this.db.insert('orders_read', {
          orderId: event.orderId,
          customerName: event.customerName,
          status: 'created',
          // ...
        });
        break;
      case 'OrderShipped':
        await this.db.update('orders_read', {
          orderId: event.orderId,
          status: 'shipped',
          shippedAt: event.timestamp,
        });
        break;
    }
  }
}

Saga Pattern para Transações Distribuídas

Quando uma operação envolve múltiplos serviços, use Sagas para coordenar.

Choreography-based Saga

Serviços reagem a eventos e emitem compensações em caso de falha
100%
Saga Pattern Coreografado

Serviços reagem a eventos e emitem compensações em caso de falha

// Cada serviço reage a eventos e pode emitir compensações
class PaymentService {
  @EventHandler('OrderCreated')
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    try {
      await this.processPayment(event.data.orderId, event.data.totalAmount);
      await this.eventBus.publish('payments', {
        eventType: 'PaymentCompleted',
        data: { orderId: event.data.orderId },
      });
    } catch (error) {
      await this.eventBus.publish('payments', {
        eventType: 'PaymentFailed',
        data: { orderId: event.data.orderId, reason: error.message },
      });
    }
  }
}

class OrderService {
  @EventHandler('PaymentFailed')
  async handlePaymentFailed(event: PaymentFailedEvent): Promise<void> {
    // Compensação: cancelar a ordem
    await this.cancelOrder(event.data.orderId);
    await this.eventBus.publish('orders', {
      eventType: 'OrderCancelled',
      data: { orderId: event.data.orderId, reason: 'Payment failed' },
    });
  }
}

Orchestration-based Saga

// Saga Orchestrator centraliza a lógica
class OrderSagaOrchestrator {
  async execute(orderId: string): Promise<void> {
    const saga = new Saga(orderId);

    try {
      // Step 1: Reserve inventory
      saga.addStep({
        execute: () => this.inventoryService.reserve(orderId),
        compensate: () => this.inventoryService.release(orderId),
      });

      // Step 2: Process payment
      saga.addStep({
        execute: () => this.paymentService.charge(orderId),
        compensate: () => this.paymentService.refund(orderId),
      });

      // Step 3: Create shipment
      saga.addStep({
        execute: () => this.shippingService.createShipment(orderId),
        compensate: () => this.shippingService.cancelShipment(orderId),
      });

      await saga.run();
    } catch (error) {
      await saga.compensate();
      throw error;
    }
  }
}
⚠️Escolhendo a Abordagem

Choreography: Mais desacoplado, melhor para fluxos simples. Orchestration: Mais fácil de entender e debugar, melhor para fluxos complexos.

Message Brokers

Apache Kafka

# docker-compose.yaml
services:
  kafka:
    image: confluentinc/cp-kafka:7.5.0
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    ports:
      - "9092:9092"
// Producer
import { Kafka } from 'kafkajs';

const kafka = new Kafka({ brokers: ['kafka:9092'] });
const producer = kafka.producer();

await producer.connect();
await producer.send({
  topic: 'orders',
  messages: [
    {
      key: orderId,  // Garante ordenação por order
      value: JSON.stringify(event),
      headers: {
        'event-type': 'OrderCreated',
        'correlation-id': correlationId,
      },
    },
  ],
});

// Consumer
const consumer = kafka.consumer({ groupId: 'inventory-service' });
await consumer.connect();
await consumer.subscribe({ topic: 'orders', fromBeginning: false });

await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const event = JSON.parse(message.value.toString());
    await processEvent(event);
  },
});

Boas Práticas


    Quer implementar arquitetura event-driven nos seus microserviços? Fale com nossos especialistas em Software Engineering.