Advanced (Actualizado: 2/6/2026)

Arquitectura orientada a eventos con Claude Code: guía práctica

Diseña eventos con Claude Code: contratos, idempotencia, reintentos, DLQ, observabilidad y errores comunes.

Arquitectura orientada a eventos con Claude Code: guía práctica

La arquitectura orientada a eventos puede hacer que un sistema sea más flexible, pero también puede ocultar dependencias peligrosas. Si solo le pides a Claude Code “hazlo desacoplado”, sin nombres de eventos, contratos de payload, reglas de reintento, plan de replay y límites de logging, el prototipo funcionará y el primer incidente será difícil de investigar.

En esta guía Claude Code no es un arquitecto incuestionable. Lo usamos como revisor y asistente de implementación. La persona responsable define los límites de negocio; Claude Code revisa si los nombres son claros, si el schema rompe consumidores, si la entrega duplicada es segura, si hay dependencia síncrona oculta, si existe DLQ y si el sistema se puede observar. Los ejemplos cubren alta de usuarios SaaS, Webhooks de pago hacia fulfillment, stream de auditoría y canal de notificaciones.

Vocabulario básico de eventos

En una arquitectura orientada a eventos, un servicio publica un hecho que ya ocurrió y otros servicios reaccionan. Un evento no es una orden. com.claudecodelab.user.created.v1 significa “se creó un usuario”, no “crea un usuario”. Esa diferencia evita que el producer conozca demasiado a los consumers.

Los términos iniciales son sencillos. Producer es quien emite el evento. Consumer es quien lo procesa. Event bus o queue es el canal de entrega. Schema es el contrato del payload, por ejemplo que userId sea un texto no vacío y email tenga formato de correo. Cuando el equipo comparte estas palabras, Claude Code puede revisar con menos suposiciones.

Como referencias oficiales, CloudEvents y CloudEvents spec sirven para un sobre de evento común. En AWS, Amazon EventBridge es una buena referencia para bus y routing. Para observabilidad, OpenTelemetry docs organiza traces, metrics y logs con un lenguaje común.

No conviene pedirle a Claude Code que invente toda la arquitectura. Dale el API existente, tablas, Webhooks, restricciones de recuperación y pídele que responda: el evento está bien nombrado, el payload es compatible hacia atrás, la entrega duplicada es segura, se puede hacer replay y se puede trazar desde producer hasta consumer.

Fija primero el contrato

El contrato va antes del handler. Sin contrato, cada consumer depende de los campos que el producer envía hoy por accidente. Un cambio pequeño puede romper onboarding, facturación, auditoría y notificaciones a la vez.

Este YAML estilo CloudEvents es un punto de partida para un evento de usuario creado. type incluye dominio, hecho y versión. idempotencykey permite ignorar duplicados seguros. correlationid conecta logs y traces de una misma solicitud.

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: "es-ES"

El payload debe tener un JSON Schema propio. Cuando Claude Code implemente producer o consumer, indícale que no dependa de campos fuera del schema y que no convierta campos opcionales en obligatorios sin una nueva versión.

{
  "$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}$" }
  }
}

Nombra eventos como hechos en pasado. user.create y sendEmail suenan a comandos. user.created, payment.authorized e invoice.finalized describen hechos. Cuidado con user.updated: parece práctico, pero obliga a cada consumer a inspeccionar el payload para saber si cambió el email, el plan o el perfil. Para cambios relevantes, usa eventos como user.email_changed.v1 o subscription.plan_changed.v1.

Dibuja el flujo de entrega

Antes de implementar, pide a Claude Code un diagrama Mermaid. El diagrama revela dependencias síncronas ocultas, puntos de retry y reglas de dead-letter más rápido que un párrafo largo.

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"]

El punto clave es que el producer no espere a todos los consumers. Si el API de registro no responde hasta que se envía el email de bienvenida, no es realmente asíncrono. Es una dependencia síncrona escondida. Si el producto necesita esa garantía, que sea un contrato síncrono explícito. Si no, diseña la pantalla y la recuperación para consistencia eventual.

Consumer mínimo en Node.js

El siguiente consumer procesa el evento de usuario creado, prepara onboarding, pone un email en cola, ignora duplicados exactos y guarda fallos en una DLQ. Usa Map para que se pueda copiar y leer; en producción cámbialo por Redis, DynamoDB, PostgreSQL u otro store compartido.

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: "es-ES",
  },
};

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

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

El prompt a Claude Code debe ser explícito: no reprocesar eventos ya exitosos, rechazar la misma idempotency key con otro payload, reintentar fallos transitorios y conservar el fallo final en DLQ. “Añade retry” no basta; puede terminar enviando emails duplicados o activando dos veces una suscripción.

Cuatro casos de uso

CasoEventosConsumersRiesgo principal
Alta SaaS y onboardinguser.created.v1, workspace.created.v1Ajustes iniciales, email, CRMEl API de alta espera a todos
Webhook de pago a fulfillmentpayment.succeeded.v1, subscription.activated.v1Permisos, factura, SlackFalta firma o idempotencia
Auditoría y event streamrole.changed.v1, api_key.revoked.v1Log append-only, búsqueda, SIEMPII en logs de larga retención
Canal de notificacionescomment.mentioned.v1, report.ready.v1Email, in-app, pushIgnorar preferencias y baja

Los Webhooks de pago encajan muy bien, pero son delicados. Revisa también Webhook implementation with Claude Code. Para contratos API, conecta con Production API development with Claude Code. Para migrar eventos v1/v2, API versioning with Claude Code aplica los mismos principios.

En auditoría, no guardes payloads completos por defecto. Usa Claude Code security audit y Claude Code security best practices para decidir qué puede quedar en logs. Para errores y excepciones, alinea el diseño con error handling patterns.

Fallos típicos

El primer fallo es un nombre vago. user.updated obliga a cada consumer a mirar el payload y decidir si le importa.

El segundo es romper el payload. Quitar email, cambiar un ID de string a objeto o hacer obligatorio un campo opcional puede romper consumers desplegados por separado. Añadir campos suele ser más seguro; borrar, cambiar tipos o cambiar significado requiere nueva versión.

El tercero es olvidar la entrega duplicada. Muchos buses entregan at-least-once: llegará al menos una vez, pero puede llegar más de una. Email, pagos, permisos y puntos necesitan idempotency key y registro persistente.

El cuarto es una dependencia síncrona oculta. Si el producer emite un evento y luego lee una tabla del consumer antes de responder, el evento no desacopló nada.

El quinto es no tener replay plan. Si un bug pierde tres horas de eventos, el equipo debe saber ventana de retención, filtro de replay, comportamiento con duplicados y reglas para suprimir efectos irreversibles.

El sexto es no observar. Logs con event id, type, correlation id, consumer, retry count y motivo de DLQ; métricas de backlog, error rate, duplicados y replay; traces que conecten la solicitud original con el consumer.

El séptimo es registrar PII. PII son datos que identifican a una persona. No pegues emails, direcciones, tokens ni datos de pago completos en logs, issues o chats.

Plantilla de revisión para Claude Code

Pide revisión antes de pedir implementación.

# 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

Si Claude Code encuentra una suposición peligrosa, ajusta el límite antes de escribir código. Después avanza en orden: schema, handler, tests y runbook.

Runbook operativo

Un sistema orientado a eventos debe poder operarse durante una falla. Añade el runbook junto con el primer 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 del merge, pregunta a Claude Code qué falla no se podría recuperar con ese runbook. Suele revelar permisos faltantes, schema drift o supuestos sobre APIs externas.

Conclusión y CTA

La arquitectura orientada a eventos funciona cuando el contrato del evento se trata como una API pública. Nombres, schema, versioning, idempotency, ordering, retries, dead-letter handling, replay y observability necesitan decisiones explícitas. Claude Code ayuda más cuando revisa esas decisiones e implementa cambios pequeños contra un contrato.

ClaudeCodeLab ayuda con formación en Claude Code, revisión de diseño event-driven, contratos Webhook/API, auditoría, runbooks de incidentes y flujos de trabajo de equipo. Si tu equipo quiere hacer Webhooks más seguros, mover notificaciones a workers asíncronos o estandarizar prompts de revisión, empieza por Claude Code training and consulting. También puedes usar la free cheat sheet y los product templates.

Masa probó este flujo en un prototipo SaaS pequeño. Cuando el event contract y la idempotency key se escribieron primero, los cambios generados por Claude Code fueron más pequeños y fáciles de revisar. En un intento anterior con solo user.updated, los consumers de notificación y auditoría empezaron a ramificarse según detalles del payload, y el replay era ambiguo. Separar nombres de eventos y añadir el runbook de DLQ dejó claro qué eventos reproducir, desde qué rango de tiempo y cuántos registros esperar.

#Claude Code #event-driven #architecture #CQRS #design patterns
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.