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 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
| Uso | Eventos | Consumers | Risco principal |
|---|---|---|---|
| Cadastro SaaS e onboarding | user.created.v1, workspace.created.v1 | Configurações, e-mail, CRM | API de cadastro espera todos |
| Webhook de pagamento para fulfillment | payment.succeeded.v1, subscription.activated.v1 | Entitlements, invoice, Slack | Falta assinatura ou idempotência |
| Auditoria e event stream | role.changed.v1, api_key.revoked.v1 | Log append-only, busca, SIEM | PII em logs longos |
| Pipeline de notificações | comment.mentioned.v1, report.ready.v1 | E-mail, in-app, push | Preferê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.
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.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Permission receipt no Claude Code: escopo, prova e rollback
Padrão de permission receipt para Claude Code: ações permitidas, limites de aprovação, comandos de prova, rollback e CTA de receita.
Agent Harness seguro para Claude Code e Codex: permissoes, verificacao e rollback
Monte uma base segura para agentes com Claude Code e Codex usando politicas, plano, verificacao e recuperacao.
Subagentes no Claude Code: guia prático para delegar trabalho com segurança
Guia prático de subagentes no Claude Code para dividir artigos e código: regras, prompts, riscos e checklist.