Advanced (Atualizado: 02/06/2026)

Arquitetura orientada a eventos com Claude Code: guia prático

Projete eventos com Claude Code: contratos, idempotência, retentativas, DLQ, observabilidade e falhas comuns.

Arquitetura orientada a eventos com Claude Code: guia prático

Arquitetura orientada a eventos pode reduzir acoplamento, mas também pode esconder dependências e dificultar incidentes. Se você pedir apenas para o Claude Code “deixar tudo event-driven”, sem nomes de evento, contrato de payload, idempotência, política de retry, dead-letter queue e limites de log, o protótipo pode funcionar enquanto a primeira falha fica quase impossível de explicar.

Neste guia, Claude Code é revisor e assistente de implementação, não um arquiteto aceito sem questionamento. A pessoa responsável decide os limites de domínio; Claude Code ajuda a revisar nomes, compatibilidade de schema, entrega duplicada, ordering, replay, DLQ e observabilidade. Os exemplos cobrem cadastro SaaS, Webhook de pagamento até fulfillment, fluxo de auditoria e pipeline de notificações.

Conceitos essenciais

Arquitetura orientada a eventos significa que um serviço publica um fato que já aconteceu, e outros serviços reagem a esse fato. Evento não é comando. com.claudecodelab.user.created.v1 quer dizer “um usuário foi criado”, não “crie um usuário”. Essa diferença mantém o producer independente dos consumers.

Quatro termos bastam no começo. Producer é quem emite o evento. Consumer é quem recebe e processa. Event bus ou queue é o caminho de entrega. Schema é o contrato do payload, por exemplo userId como string não vazia e email em formato de e-mail. Quando o time usa esses termos, Claude Code revisa com menos chute.

Como fontes oficiais, CloudEvents e CloudEvents spec ajudam a padronizar o envelope do evento. Em AWS, Amazon EventBridge é uma referência prática para bus e roteamento. Para observabilidade, OpenTelemetry docs organiza traces, metrics e logs.

Não peça ao Claude Code para inventar a arquitetura inteira. Entregue a ele APIs existentes, tabelas, Webhooks e regras de recuperação. Peça revisão: o nome é claro, o payload é compatível, duplicatas são seguras, existe replay e o evento pode ser rastreado do producer até o consumer?

Contrato antes de código

O contrato vem antes do handler. Sem contrato, cada consumer passa a depender silenciosamente do que o producer envia hoje. Uma pequena mudança pode quebrar onboarding, billing, auditoria e notificações.

Este YAML no estilo CloudEvents é um template de evento de usuário criado em um SaaS. type inclui domínio, fato e versão. idempotencykey evita efeitos duplicados quando o evento chega mais de uma vez. correlationid liga logs e traces da mesma requisição original.

specversion: "1.0"
id: "evt_01JZ0YV8Y9N3A7Z7K6Y1G9X2Q4"
type: "com.claudecodelab.user.created.v1"
source: "/services/identity"
subject: "users/usr_123"
time: "2026-06-02T09:30:00Z"
datacontenttype: "application/json"
dataschema: "https://example.com/schemas/user-created.v1.json"
idempotencykey: "user.created:usr_123:2026-06-02"
correlationid: "req_7fc42b"
data:
  userId: "usr_123"
  email: "masa@example.com"
  plan: "starter"
  locale: "pt-BR"

O payload deve ter um JSON Schema separado. Ao pedir implementação ao Claude Code, deixe claro que ele não deve depender de campos fora do schema, nem tornar obrigatório um campo opcional sem nova versão.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/user-created.v1.json",
  "title": "UserCreatedV1",
  "type": "object",
  "additionalProperties": false,
  "required": ["userId", "email", "plan", "locale"],
  "properties": {
    "userId": { "type": "string", "minLength": 1 },
    "email": { "type": "string", "format": "email" },
    "plan": { "type": "string", "enum": ["free", "starter", "pro"] },
    "locale": { "type": "string", "pattern": "^[a-z]{2}-[A-Z]{2}$" }
  }
}

Nomeie eventos como fatos no passado. user.create e sendEmail parecem comandos. user.created, payment.authorized e invoice.finalized descrevem fatos. user.updated parece econômico, mas obriga cada consumer a inspecionar o payload para saber se foi e-mail, plano ou perfil. Para mudanças importantes, prefira user.email_changed.v1 ou subscription.plan_changed.v1.

Desenhe o fluxo

Antes de implementar, peça um diagrama Mermaid ao Claude Code. Ele mostra dependências síncronas escondidas, pontos de retry e DLQ com mais clareza que texto.

flowchart LR
  A["Identity API<br/>producer"] --> B["Event bus<br/>filter and route"]
  B --> C["Onboarding consumer<br/>workspace setup"]
  B --> D["Email consumer<br/>welcome message"]
  B --> E["Audit consumer<br/>append-only log"]
  C --> F["Idempotency store"]
  D --> F
  C --> G["Dead-letter queue"]
  D --> G
  B --> H["OpenTelemetry<br/>traces metrics logs"]

O ponto de revisão é que o producer não espere todos os consumers. Se a API de cadastro só responde depois que o e-mail de boas-vindas é enviado, não é realmente assíncrona. É uma dependência síncrona escondida. Se ela for necessária, exponha no contrato da API; se não for, desenhe a experiência para consistência eventual.

Consumer mínimo em Node.js

O consumer abaixo processa o evento de usuário criado, cria onboarding, coloca e-mail de boas-vindas na fila, ignora duplicatas exatas e envia falhas para a dead-letter queue. Ele usa Map para ficar simples; em produção, use Redis, DynamoDB, PostgreSQL ou outro store compartilhado.

const crypto = require("node:crypto");

const processedEvents = new Map();
const deadLetterQueue = [];

function payloadHash(payload) {
  return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
}

function eventKey(event) {
  return event.idempotencykey || `${event.type}:${event.id}`;
}

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function withRetry(operation, options = {}) {
  const attempts = options.attempts ?? 3;
  const delayMs = options.delayMs ?? 250;
  let lastError;

  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      if (attempt === attempts) break;
      await wait(delayMs * attempt);
    }
  }

  throw lastError;
}

async function handleUserCreated(event, services) {
  if (event.specversion !== "1.0") {
    throw new Error(`Unsupported CloudEvents version: ${event.specversion}`);
  }

  if (event.type !== "com.claudecodelab.user.created.v1") {
    throw new Error(`Unexpected event type: ${event.type}`);
  }

  const key = eventKey(event);
  const currentHash = payloadHash(event.data);
  const existing = processedEvents.get(key);

  if (existing?.status === "succeeded" && existing.payloadHash === currentHash) {
    return { status: "duplicate_ignored", key };
  }

  if (existing && existing.payloadHash !== currentHash) {
    throw new Error("Idempotency key reused with a different payload");
  }

  processedEvents.set(key, {
    status: "processing",
    payloadHash: currentHash,
    updatedAt: new Date().toISOString(),
  });

  try {
    await withRetry(() => services.createOnboardingWorkspace(event.data.userId), {
      attempts: 3,
      delayMs: 200,
    });

    await withRetry(
      () =>
        services.enqueueWelcomeEmail({
          userId: event.data.userId,
          email: event.data.email,
          correlationId: event.correlationid,
        }),
      { attempts: 3, delayMs: 200 },
    );

    processedEvents.set(key, {
      status: "succeeded",
      payloadHash: currentHash,
      updatedAt: new Date().toISOString(),
    });

    return { status: "processed", key };
  } catch (error) {
    processedEvents.set(key, {
      status: "failed",
      payloadHash: currentHash,
      updatedAt: new Date().toISOString(),
      errorMessage: error.message,
    });

    deadLetterQueue.push({
      key,
      event,
      failedAt: new Date().toISOString(),
      errorMessage: error.message,
    });

    throw error;
  }
}

const services = {
  async createOnboardingWorkspace(userId) {
    console.log("workspace ready", { userId });
  },
  async enqueueWelcomeEmail(message) {
    console.log("email queued", {
      userId: message.userId,
      correlationId: message.correlationId,
    });
  },
};

const exampleEvent = {
  specversion: "1.0",
  id: "evt_01JZ0YV8Y9N3A7Z7K6Y1G9X2Q4",
  type: "com.claudecodelab.user.created.v1",
  source: "/services/identity",
  time: "2026-06-02T09:30:00Z",
  idempotencykey: "user.created:usr_123:2026-06-02",
  correlationid: "req_7fc42b",
  data: {
    userId: "usr_123",
    email: "masa@example.com",
    plan: "starter",
    locale: "pt-BR",
  },
};

handleUserCreated(exampleEvent, services)
  .then((result) => console.log(result))
  .catch((error) => console.error(error));

module.exports = { handleUserCreated, withRetry, deadLetterQueue };

O prompt precisa ser específico: não reprocessar evento já concluído, rejeitar a mesma idempotency key com payload diferente, retentar falha transitória e manter a falha final na DLQ. “Adicionar retry” é vago e pode gerar e-mails ou permissões duplicadas.

Quatro usos práticos

UsoEventosConsumersRisco principal
Cadastro SaaS e onboardinguser.created.v1, workspace.created.v1Configurações, e-mail, CRMAPI de cadastro espera todos
Webhook de pagamento para fulfillmentpayment.succeeded.v1, subscription.activated.v1Entitlements, invoice, SlackFalta assinatura ou idempotência
Auditoria e event streamrole.changed.v1, api_key.revoked.v1Log append-only, busca, SIEMPII em logs longos
Pipeline de notificaçõescomment.mentioned.v1, report.ready.v1E-mail, in-app, pushPreferências e opt-out ignorados

Webhooks de pagamento combinam bem com eventos, mas exigem rigor. Veja também Webhook implementation with Claude Code. Para contrato de API, use Production API development with Claude Code. Para migração v1/v2, API versioning with Claude Code segue a mesma lógica.

Em auditoria, não grave payload completo por padrão. Use Claude Code security audit e Claude Code security best practices para decidir campos permitidos. Respostas de erro e exceções devem seguir error handling patterns.

Armadilhas comuns

A primeira é nome vago. user.updated obriga cada consumer a abrir o payload e decidir se deve agir.

A segunda é mudança quebradora de payload. Remover email, trocar string por objeto ou tornar obrigatório um campo opcional pode quebrar consumers independentes. Adições costumam ser seguras; remoção, tipo e semântica pedem nova versão.

A terceira é esquecer duplicatas. Muitos sistemas usam at-least-once delivery: o evento chega pelo menos uma vez, mas pode chegar mais de uma. E-mail, pagamento, permissões e pontos precisam de idempotency key e registro persistente.

A quarta é dependência síncrona escondida. Se o producer emite evento e depois lê a tabela do consumer antes de responder, o acoplamento continua.

A quinta é não ter replay plan. Se um bug derruba três horas de eventos, o time precisa saber retenção, filtro de replay, comportamento com duplicatas e supressão de efeitos irreversíveis.

A sexta é baixa observabilidade. Logs devem ter event id, type, correlation id, consumer, retry count e motivo de DLQ. Métricas devem cobrir backlog age, taxa de erro, duplicatas e replay.

A sétima é logar PII. PII são dados que identificam uma pessoa, como e-mail, nome, endereço, pagamento e tokens. Prefira event id e userId, mascare campos sensíveis e defina retenção.

Template de revisão para Claude Code

Peça revisão antes de pedir código.

# Claude Code EDA review checklist

Scope:
- event contract: schemas/user-created.v1.json
- producer: services/identity
- consumers: onboarding, email, audit-log

Please review:
- Is the event name a past-tense fact?
- Is the payload change backward compatible for existing consumers?
- Is there an idempotency key, and does duplicate delivery avoid double side effects?
- Does any consumer call back into the producer synchronously?
- Are retry count, backoff, and dead-letter rules explicit?
- Can replay run without duplicate email, payment, or irreversible effects?
- Do logs avoid PII and secrets?
- Can OpenTelemetry show event id, correlation id, and consumer name?

Output:
- P0/P1/P2 risks
- Files that should change
- Tests that should be added
- Open decisions a human must make

Se Claude Code encontrar uma premissa perigosa, corrija a fronteira antes de implementar. Depois avance em schema, handler, testes e runbook.

Runbook de operação

Um sistema event-driven só vale se puder ser operado durante falhas. Inclua um runbook com o primeiro consumer.

# Runbook: event backlog or DLQ growth

## Symptoms
- Queue age is over 5 minutes
- Dead-letter queue has more than 10 messages
- Consumer error rate is over 2 percent for 10 minutes

## First checks
1. Identify event type, consumer name, and correlation id.
2. Check whether the failure is validation, downstream timeout, or permission.
3. Confirm whether the producer is still publishing new events.
4. Stop replay if the event triggers email, payment, or irreversible side effects.

## Recovery
1. Fix the consumer or downstream dependency.
2. Replay a small batch with idempotency enabled.
3. Compare processed count, duplicate count, and DLQ count.
4. Resume normal processing.
5. Write the incident note with event ids, time range, and customer impact.

## Never do
- Do not edit payloads manually without recording the reason.
- Do not replay payment or email events without suppression rules.
- Do not paste full payloads with PII into chat or issue trackers.

Antes do merge, pergunte ao Claude Code: “qual falha este runbook não recupera?” A resposta costuma revelar permissão faltando, schema drift ou dependência externa.

Conclusão e CTA

Arquitetura orientada a eventos funciona quando o contrato do evento é tratado como uma API pública. Nomes, schema, versioning, idempotency, ordering, retries, dead-letter handling, replay e observability precisam de decisões explícitas. Claude Code é mais útil quando revisa essas decisões e implementa mudanças pequenas contra um contrato claro.

ClaudeCodeLab ajuda com treinamento Claude Code, revisão de design event-driven, contratos Webhook/API, auditoria, runbooks de incidente e workflows de equipe. Se seu time quer tornar Webhooks mais seguros, mover notificações para workers assíncronos ou padronizar prompts de revisão, comece por Claude Code training and consulting. Para material de autoestudo, veja a free cheat sheet e os product templates.

Masa testou esse fluxo em um pequeno protótipo SaaS. Quando event contract e idempotency key vieram primeiro, as mudanças geradas pelo Claude Code ficaram menores e mais fáceis de revisar. Em um teste anterior com apenas user.updated, os consumers de notificação e auditoria começaram a criar ramificações com base no payload, e o replay ficou ambíguo. Ao separar nomes de eventos e adicionar o runbook de DLQ, ficou claro quais eventos reproduzir, a partir de qual janela de tempo e quantos registros esperar.

#Claude Code #event-driven #architecture #CQRS #design patterns
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.